@mmapp/react 0.1.0-alpha.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 (98) hide show
  1. package/dist/atoms/index.d.mts +424 -0
  2. package/dist/atoms/index.d.ts +424 -0
  3. package/dist/atoms/index.js +195 -0
  4. package/dist/atoms/index.mjs +110 -0
  5. package/dist/chunk-2VJQJM7S.mjs +119 -0
  6. package/dist/index.d.mts +6999 -0
  7. package/dist/index.d.ts +6999 -0
  8. package/dist/index.js +8247 -0
  9. package/dist/index.mjs +8016 -0
  10. package/package.json +37 -0
  11. package/package.json.backup +41 -0
  12. package/src/Blueprint.ts +216 -0
  13. package/src/__tests__/Blueprint.test.ts +106 -0
  14. package/src/__tests__/action-context.test.ts +166 -0
  15. package/src/__tests__/actionCreators.test.ts +179 -0
  16. package/src/__tests__/builders.test.ts +336 -0
  17. package/src/__tests__/defineBlueprint-composition.test.ts +106 -0
  18. package/src/__tests__/factories.test.ts +229 -0
  19. package/src/__tests__/loader.test.ts +159 -0
  20. package/src/__tests__/logger.test.ts +70 -0
  21. package/src/__tests__/type-inference.test.ts +160 -0
  22. package/src/__tests__/typed-transitions.test.ts +126 -0
  23. package/src/__tests__/useModuleConfig.test.ts +61 -0
  24. package/src/actionCreators.ts +132 -0
  25. package/src/actions.ts +547 -0
  26. package/src/atoms/index.ts +600 -0
  27. package/src/authoring.ts +92 -0
  28. package/src/browser-player.ts +783 -0
  29. package/src/builders.ts +1342 -0
  30. package/src/components/ExperienceWorkflowBridge.tsx +123 -0
  31. package/src/components/PlayerProvider.tsx +43 -0
  32. package/src/components/atoms/index.tsx +269 -0
  33. package/src/components/index.ts +36 -0
  34. package/src/conditions.ts +692 -0
  35. package/src/config/defineBlueprint.ts +329 -0
  36. package/src/config/defineModel.ts +753 -0
  37. package/src/config/defineWorkspace.ts +24 -0
  38. package/src/core/WorkflowRuntime.ts +153 -0
  39. package/src/factories.ts +425 -0
  40. package/src/grammar/index.ts +173 -0
  41. package/src/hooks/index.ts +106 -0
  42. package/src/hooks/useAuth.ts +288 -0
  43. package/src/hooks/useChannel.ts +304 -0
  44. package/src/hooks/useComputed.ts +154 -0
  45. package/src/hooks/useDomainSubscription.ts +110 -0
  46. package/src/hooks/useDuringAction.ts +99 -0
  47. package/src/hooks/useExperienceState.ts +59 -0
  48. package/src/hooks/useExpressionLibrary.ts +129 -0
  49. package/src/hooks/useForm.ts +352 -0
  50. package/src/hooks/useGeolocation.ts +207 -0
  51. package/src/hooks/useMapView.ts +259 -0
  52. package/src/hooks/useMiddleware.ts +291 -0
  53. package/src/hooks/useModel.ts +363 -0
  54. package/src/hooks/useModule.ts +59 -0
  55. package/src/hooks/useModuleConfig.ts +61 -0
  56. package/src/hooks/useMutation.ts +237 -0
  57. package/src/hooks/useNotification.ts +151 -0
  58. package/src/hooks/useOnChange.ts +30 -0
  59. package/src/hooks/useOnEnter.ts +59 -0
  60. package/src/hooks/useOnEvent.ts +37 -0
  61. package/src/hooks/useOnExit.ts +27 -0
  62. package/src/hooks/useOnTransition.ts +30 -0
  63. package/src/hooks/usePackage.ts +128 -0
  64. package/src/hooks/useParams.ts +33 -0
  65. package/src/hooks/usePlayer.ts +308 -0
  66. package/src/hooks/useQuery.ts +184 -0
  67. package/src/hooks/useRealtimeQuery.ts +222 -0
  68. package/src/hooks/useRole.ts +191 -0
  69. package/src/hooks/useRouteParams.ts +100 -0
  70. package/src/hooks/useRouter.ts +347 -0
  71. package/src/hooks/useServerAction.ts +178 -0
  72. package/src/hooks/useServerState.ts +284 -0
  73. package/src/hooks/useToast.ts +164 -0
  74. package/src/hooks/useTransition.ts +39 -0
  75. package/src/hooks/useView.ts +102 -0
  76. package/src/hooks/useWhileIn.ts +48 -0
  77. package/src/hooks/useWorkflow.ts +63 -0
  78. package/src/index.ts +465 -0
  79. package/src/loader/experience-workflow-loader.ts +192 -0
  80. package/src/loader/index.ts +6 -0
  81. package/src/local/LocalEngine.ts +388 -0
  82. package/src/local/LocalEngineAdapter.ts +175 -0
  83. package/src/local/LocalEngineContext.ts +30 -0
  84. package/src/logger.ts +37 -0
  85. package/src/mixins.ts +1160 -0
  86. package/src/providers/RuntimeContext.ts +20 -0
  87. package/src/providers/WorkflowProvider.tsx +28 -0
  88. package/src/routing/instance-key.ts +107 -0
  89. package/src/server/transition-context.ts +172 -0
  90. package/src/testing/index.ts +9 -0
  91. package/src/testing/useBlueprintTestRunner.ts +91 -0
  92. package/src/testing/useGraphAnalysis.ts +18 -0
  93. package/src/testing/useTestRunner.ts +77 -0
  94. package/src/testing.ts +995 -0
  95. package/src/types/workflow-inference.ts +158 -0
  96. package/src/types.ts +114 -0
  97. package/tsconfig.json +27 -0
  98. package/vitest.config.ts +8 -0
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createActions } from '../actionCreators';
3
+ import { setField } from '../actions';
4
+ import { model, state, transition } from '../builders';
5
+
6
+ // =============================================================================
7
+ // createActions
8
+ // =============================================================================
9
+
10
+ describe('createActions', () => {
11
+ const actions = createActions({
12
+ authenticate: { handler: 'actions/auth.server.ts' },
13
+ destroySession: { handler: 'actions/auth.server.ts' },
14
+ resetPassword: { handler: 'actions/auth.server.ts', functionName: 'handleReset' },
15
+ sendEmail: { handler: 'actions/email.server.ts', mode: 'manual' },
16
+ });
17
+
18
+ it('returns an object with a creator for each manifest entry', () => {
19
+ expect(typeof actions.authenticate).toBe('function');
20
+ expect(typeof actions.destroySession).toBe('function');
21
+ expect(typeof actions.resetPassword).toBe('function');
22
+ expect(typeof actions.sendEmail).toBe('function');
23
+ });
24
+
25
+ it('produces a valid ActionDefinition with no config', () => {
26
+ const result = actions.destroySession();
27
+
28
+ expect(result).toEqual({
29
+ id: 'server-destroy-session',
30
+ type: 'server:destroySession',
31
+ mode: 'auto',
32
+ config: {
33
+ __handler: 'actions/auth.server.ts',
34
+ },
35
+ });
36
+ });
37
+
38
+ it('produces a valid ActionDefinition with config', () => {
39
+ const result = actions.authenticate({ method: 'login', email: 'test@example.com' });
40
+
41
+ expect(result).toEqual({
42
+ id: 'server-authenticate',
43
+ type: 'server:authenticate',
44
+ mode: 'auto',
45
+ config: {
46
+ method: 'login',
47
+ email: 'test@example.com',
48
+ __handler: 'actions/auth.server.ts',
49
+ },
50
+ });
51
+ });
52
+
53
+ it('includes functionName in config when specified', () => {
54
+ const result = actions.resetPassword({ token: 'abc123' });
55
+
56
+ expect(result.config).toEqual({
57
+ token: 'abc123',
58
+ __handler: 'actions/auth.server.ts',
59
+ __functionName: 'handleReset',
60
+ });
61
+ });
62
+
63
+ it('respects the mode from the manifest entry', () => {
64
+ const result = actions.sendEmail();
65
+ expect(result.mode).toBe('manual');
66
+ });
67
+
68
+ it('defaults mode to auto when not specified', () => {
69
+ const result = actions.authenticate();
70
+ expect(result.mode).toBe('auto');
71
+ });
72
+
73
+ it('generates kebab-case IDs from camelCase names', () => {
74
+ const result = actions.destroySession();
75
+ expect(result.id).toBe('server-destroy-session');
76
+ });
77
+
78
+ it('preserves the original action name in the type', () => {
79
+ const result = actions.resetPassword();
80
+ expect(result.type).toBe('server:resetPassword');
81
+ });
82
+
83
+ it('produces actions compatible with .do() / onEnter / onExit arrays', () => {
84
+ const result = actions.authenticate({ method: 'login' });
85
+
86
+ // ActionDefinition requires id, type, and optional mode/config/condition
87
+ expect(result).toHaveProperty('id');
88
+ expect(result).toHaveProperty('type');
89
+ expect(typeof result.id).toBe('string');
90
+ expect(typeof result.type).toBe('string');
91
+ });
92
+
93
+ it('works with an empty manifest', () => {
94
+ const empty = createActions({});
95
+ expect(Object.keys(empty)).toHaveLength(0);
96
+ });
97
+
98
+ it('each call produces a fresh ActionDefinition', () => {
99
+ const a = actions.authenticate({ method: 'login' });
100
+ const b = actions.authenticate({ method: 'register' });
101
+
102
+ expect(a).not.toBe(b);
103
+ expect(a.config!.method).toBe('login');
104
+ expect(b.config!.method).toBe('register');
105
+ });
106
+
107
+ it('user config does not overwrite __handler', () => {
108
+ const result = actions.authenticate({ __handler: 'hacked.ts' });
109
+ // User config is spread first, then __handler overwrites
110
+ expect(result.config!.__handler).toBe('actions/auth.server.ts');
111
+ });
112
+ });
113
+
114
+ // =============================================================================
115
+ // Integration: createActions + builder API
116
+ // =============================================================================
117
+
118
+ describe('createActions integration with model builder', () => {
119
+ const actions = createActions({
120
+ authenticate: { handler: 'actions/auth.server.ts' },
121
+ destroySession: { handler: 'actions/auth.server.ts' },
122
+ });
123
+
124
+ it('works with TransitionBuilder.do()', () => {
125
+ const result = model('auth')
126
+ .state('unauthenticated', state.initial())
127
+ .state('authenticating')
128
+ .state('authenticated')
129
+ .transition('login',
130
+ transition.from('unauthenticated').to('authenticating')
131
+ .do(actions.authenticate({ method: 'login' }))
132
+ )
133
+ .build();
134
+
135
+ expect(result.transitions.login.actions).toHaveLength(1);
136
+ expect(result.transitions.login.actions![0].type).toBe('server:authenticate');
137
+ expect(result.transitions.login.actions![0].config).toMatchObject({ method: 'login' });
138
+ });
139
+
140
+ it('works with StateBuilder.onEnter()', () => {
141
+ const result = model('auth')
142
+ .state('authenticated', state.initial()
143
+ .onEnter(actions.authenticate({ method: 'token' }))
144
+ )
145
+ .build();
146
+
147
+ expect(result.states.authenticated.onEnter).toHaveLength(1);
148
+ expect(result.states.authenticated.onEnter![0].type).toBe('server:authenticate');
149
+ });
150
+
151
+ it('works with StateBuilder.onExit()', () => {
152
+ const result = model('auth')
153
+ .state('authenticated', state.initial()
154
+ .onExit(actions.destroySession())
155
+ )
156
+ .build();
157
+
158
+ expect(result.states.authenticated.onExit).toHaveLength(1);
159
+ expect(result.states.authenticated.onExit![0].type).toBe('server:destroySession');
160
+ });
161
+
162
+ it('works alongside plain action helpers', () => {
163
+ const result = model('auth')
164
+ .state('unauthenticated', state.initial())
165
+ .state('authenticated')
166
+ .transition('login',
167
+ transition.from('unauthenticated').to('authenticated')
168
+ .do(
169
+ actions.authenticate({ method: 'login' }),
170
+ setField('lastLogin', 'NOW()'),
171
+ )
172
+ )
173
+ .build();
174
+
175
+ expect(result.transitions.login.actions).toHaveLength(2);
176
+ expect(result.transitions.login.actions![0].type).toBe('server:authenticate');
177
+ expect(result.transitions.login.actions![1].type).toBe('set_field');
178
+ });
179
+ });
@@ -0,0 +1,336 @@
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
+ });
@@ -0,0 +1,106 @@
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
+ });