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

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 +1378 -94
  3. package/dist/index.d.ts +1378 -94
  4. package/dist/index.js +1094 -1309
  5. package/dist/index.mjs +1038 -1296
  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,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
- });
@@ -1,159 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { validateExperienceWorkflow, normalizeDefinition } from '../loader';
3
-
4
- const validDef = {
5
- id: 'test-1',
6
- slug: 'test-workflow',
7
- states: [
8
- { name: 'start', type: 'START' },
9
- { name: 'active', type: 'REGULAR' },
10
- { name: 'done', type: 'END' },
11
- ],
12
- transitions: [
13
- { name: 'begin', from: ['start'], to: 'active' },
14
- { name: 'finish', from: ['active'], to: 'done' },
15
- ],
16
- };
17
-
18
- describe('validateExperienceWorkflow', () => {
19
- it('validates a correct definition', () => {
20
- const result = validateExperienceWorkflow(validDef);
21
- expect(result.valid).toBe(true);
22
- expect(result.errors).toHaveLength(0);
23
- });
24
-
25
- it('rejects non-object input', () => {
26
- expect(validateExperienceWorkflow(null).valid).toBe(false);
27
- expect(validateExperienceWorkflow('string').valid).toBe(false);
28
- expect(validateExperienceWorkflow(42).valid).toBe(false);
29
- });
30
-
31
- it('requires id and slug', () => {
32
- const result = validateExperienceWorkflow({ states: [{ name: 'x', type: 'START' }], transitions: [] });
33
- expect(result.valid).toBe(false);
34
- expect(result.errors.some(e => e.includes('id'))).toBe(true);
35
- expect(result.errors.some(e => e.includes('slug'))).toBe(true);
36
- });
37
-
38
- it('requires at least one state', () => {
39
- const result = validateExperienceWorkflow({ id: '1', slug: 's', states: [], transitions: [] });
40
- expect(result.valid).toBe(false);
41
- expect(result.errors.some(e => e.includes('at least one state'))).toBe(true);
42
- });
43
-
44
- it('requires a START state', () => {
45
- const result = validateExperienceWorkflow({
46
- id: '1', slug: 's',
47
- states: [{ name: 'x', type: 'REGULAR' }],
48
- transitions: [],
49
- });
50
- expect(result.valid).toBe(false);
51
- expect(result.errors.some(e => e.includes('START'))).toBe(true);
52
- });
53
-
54
- it('detects duplicate state names', () => {
55
- const result = validateExperienceWorkflow({
56
- id: '1', slug: 's',
57
- states: [{ name: 'x', type: 'START' }, { name: 'x', type: 'REGULAR' }],
58
- transitions: [],
59
- });
60
- expect(result.valid).toBe(false);
61
- expect(result.errors.some(e => e.includes('Duplicate'))).toBe(true);
62
- });
63
-
64
- it('detects unknown transition target states', () => {
65
- const result = validateExperienceWorkflow({
66
- id: '1', slug: 's',
67
- states: [{ name: 'start', type: 'START' }],
68
- transitions: [{ name: 't', from: ['start'], to: 'nonexistent' }],
69
- });
70
- expect(result.valid).toBe(false);
71
- expect(result.errors.some(e => e.includes('unknown state'))).toBe(true);
72
- });
73
-
74
- it('detects unknown transition from states', () => {
75
- const result = validateExperienceWorkflow({
76
- id: '1', slug: 's',
77
- states: [{ name: 'start', type: 'START' }, { name: 'end', type: 'END' }],
78
- transitions: [{ name: 't', from: ['ghost'], to: 'end' }],
79
- });
80
- expect(result.valid).toBe(false);
81
- expect(result.errors.some(e => e.includes('unknown "from" state'))).toBe(true);
82
- });
83
-
84
- it('validates on_event subscriptions', () => {
85
- const result = validateExperienceWorkflow({
86
- id: '1', slug: 's',
87
- states: [{
88
- name: 'start', type: 'START',
89
- on_event: [{ actions: [{ type: 'log', config: {} }] }], // missing match
90
- }],
91
- transitions: [],
92
- });
93
- expect(result.valid).toBe(false);
94
- expect(result.errors.some(e => e.includes('match'))).toBe(true);
95
- });
96
-
97
- it('warns on on_event with empty actions', () => {
98
- const result = validateExperienceWorkflow({
99
- id: '1', slug: 's',
100
- states: [{
101
- name: 'start', type: 'START',
102
- on_event: [{ match: 'test.*', actions: [] }],
103
- }],
104
- transitions: [],
105
- });
106
- expect(result.valid).toBe(true); // warning, not error
107
- expect(result.warnings.some(w => w.includes('no actions'))).toBe(true);
108
- });
109
- });
110
-
111
- describe('normalizeDefinition', () => {
112
- it('normalizes a valid definition', () => {
113
- const def = normalizeDefinition(validDef);
114
- expect(def.id).toBe('test-1');
115
- expect(def.slug).toBe('test-workflow');
116
- expect(def.states).toHaveLength(3);
117
- expect(def.transitions).toHaveLength(2);
118
- });
119
-
120
- it('defaults state type to REGULAR', () => {
121
- const def = normalizeDefinition({
122
- id: '1', slug: 's',
123
- states: [{ name: 'x' }],
124
- transitions: [],
125
- });
126
- expect(def.states[0].type).toBe('REGULAR');
127
- });
128
-
129
- it('coerces string "from" to array', () => {
130
- const def = normalizeDefinition({
131
- id: '1', slug: 's',
132
- states: [{ name: 'a', type: 'START' }, { name: 'b', type: 'END' }],
133
- transitions: [{ name: 't', from: 'a', to: 'b' }],
134
- });
135
- expect(def.transitions[0].from).toEqual(['a']);
136
- });
137
-
138
- it('normalizes on_event with "pattern" field to "match"', () => {
139
- const def = normalizeDefinition({
140
- id: '1', slug: 's',
141
- states: [{
142
- name: 'x', type: 'START',
143
- on_event: [{ pattern: 'test.*', actions: [{ type: 'log', config: {} }] }],
144
- }],
145
- transitions: [],
146
- });
147
- expect(def.states[0].on_event?.[0].match).toBe('test.*');
148
- });
149
-
150
- it('preserves state_views and experience metadata', () => {
151
- const def = normalizeDefinition({
152
- ...validDef,
153
- state_views: { active: { title: 'Active View' } },
154
- experience: { default_layout: 'grid' },
155
- });
156
- expect(def.state_views?.active.title).toBe('Active View');
157
- expect(def.experience?.default_layout).toBe('grid');
158
- });
159
- });
@@ -1,70 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { setPlayerDebug, isPlayerDebug, playerLog } from '../logger';
3
-
4
- describe('Player Logger', () => {
5
- beforeEach(() => {
6
- setPlayerDebug(false);
7
- });
8
-
9
- it('toggles debug mode', () => {
10
- expect(isPlayerDebug()).toBe(false);
11
- setPlayerDebug(true);
12
- expect(isPlayerDebug()).toBe(true);
13
- });
14
-
15
- it('suppresses debug logs when debug is off', () => {
16
- const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
17
- playerLog({ level: 'debug', category: 'event_match', message: 'test' });
18
- expect(spy).not.toHaveBeenCalled();
19
- spy.mockRestore();
20
- });
21
-
22
- it('emits debug logs when debug is on', () => {
23
- setPlayerDebug(true);
24
- const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
25
- playerLog({ level: 'debug', category: 'event_match', message: 'test' });
26
- expect(spy).toHaveBeenCalledOnce();
27
- spy.mockRestore();
28
- });
29
-
30
- it('emits warn logs regardless of debug setting', () => {
31
- const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
32
- playerLog({ level: 'warn', category: 'action_dispatch', message: 'warning' });
33
- expect(spy).toHaveBeenCalledOnce();
34
- spy.mockRestore();
35
- });
36
-
37
- it('emits error logs', () => {
38
- const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
39
- playerLog({ level: 'error', category: 'transition', message: 'error msg' });
40
- expect(spy).toHaveBeenCalledOnce();
41
- spy.mockRestore();
42
- });
43
-
44
- it('emits info logs', () => {
45
- const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
46
- playerLog({ level: 'info', category: 'lifecycle', message: 'info msg' });
47
- expect(spy).toHaveBeenCalledOnce();
48
- spy.mockRestore();
49
- });
50
-
51
- it('includes category prefix in log message', () => {
52
- const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
53
- playerLog({ level: 'warn', category: 'event_match', message: 'matched' });
54
- expect(spy).toHaveBeenCalledWith(
55
- expect.stringContaining('[player-web:event_match]'),
56
- expect.anything(),
57
- );
58
- spy.mockRestore();
59
- });
60
-
61
- it('passes data object to console', () => {
62
- const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
63
- playerLog({ level: 'info', category: 'lifecycle', message: 'test', data: { key: 'val' } });
64
- expect(spy).toHaveBeenCalledWith(
65
- expect.any(String),
66
- { key: 'val' },
67
- );
68
- spy.mockRestore();
69
- });
70
- });
@@ -1,160 +0,0 @@
1
- import { describe, it, expect, expectTypeOf } from 'vitest';
2
- import { defineModel } from '../config/defineModel';
3
- import type {
4
- InferFields,
5
- InferTransitions,
6
- InferStates,
7
- InferSlug,
8
- InferFieldNames,
9
- } from '../types/workflow-inference';
10
-
11
- // =============================================================================
12
- // Test model definition
13
- // =============================================================================
14
-
15
- const employeeModel = defineModel({
16
- slug: 'employee',
17
- version: '1.0.0',
18
- category: 'data',
19
- fields: {
20
- name: { type: 'string', required: true },
21
- email: { type: 'email', required: true },
22
- salary: { type: 'currency', default: 0 },
23
- isActive: { type: 'boolean', default: true },
24
- startDate: { type: 'date' },
25
- tags: { type: 'array' },
26
- metadata: { type: 'object' },
27
- department: { type: 'string', enum: ['engineering', 'sales', 'hr'] as const },
28
- rating: { type: 'rating' },
29
- },
30
- states: {
31
- onboarding: { type: 'initial' },
32
- active: {},
33
- on_leave: {},
34
- terminated: { type: 'end' },
35
- },
36
- transitions: {
37
- activate: { from: 'onboarding', to: 'active' },
38
- request_leave: { from: 'active', to: 'on_leave' },
39
- return_from_leave: { from: 'on_leave', to: 'active' },
40
- terminate: { from: ['active', 'on_leave'], to: 'terminated' },
41
- },
42
- } as const);
43
-
44
- type EmployeeFields = InferFields<typeof employeeModel>;
45
- type EmployeeTransitions = InferTransitions<typeof employeeModel>;
46
- type EmployeeStates = InferStates<typeof employeeModel>;
47
-
48
- // =============================================================================
49
- // Type-level tests
50
- // =============================================================================
51
-
52
- describe('InferFields', () => {
53
- it('maps string-family types to string', () => {
54
- expectTypeOf<EmployeeFields['name']>().toEqualTypeOf<string>();
55
- expectTypeOf<EmployeeFields['email']>().toEqualTypeOf<string>();
56
- expectTypeOf<EmployeeFields['department']>().toEqualTypeOf<string>();
57
- expectTypeOf<EmployeeFields['startDate']>().toEqualTypeOf<string>();
58
- });
59
-
60
- it('maps numeric types to number', () => {
61
- expectTypeOf<EmployeeFields['salary']>().toEqualTypeOf<number>();
62
- expectTypeOf<EmployeeFields['rating']>().toEqualTypeOf<number>();
63
- });
64
-
65
- it('maps boolean type to boolean', () => {
66
- expectTypeOf<EmployeeFields['isActive']>().toEqualTypeOf<boolean>();
67
- });
68
-
69
- it('maps complex types correctly', () => {
70
- expectTypeOf<EmployeeFields['tags']>().toEqualTypeOf<unknown[]>();
71
- expectTypeOf<EmployeeFields['metadata']>().toEqualTypeOf<Record<string, unknown>>();
72
- });
73
-
74
- it('produces an object with all field keys', () => {
75
- expectTypeOf<EmployeeFields>().toHaveProperty('name');
76
- expectTypeOf<EmployeeFields>().toHaveProperty('email');
77
- expectTypeOf<EmployeeFields>().toHaveProperty('salary');
78
- expectTypeOf<EmployeeFields>().toHaveProperty('isActive');
79
- expectTypeOf<EmployeeFields>().toHaveProperty('startDate');
80
- expectTypeOf<EmployeeFields>().toHaveProperty('tags');
81
- expectTypeOf<EmployeeFields>().toHaveProperty('metadata');
82
- expectTypeOf<EmployeeFields>().toHaveProperty('department');
83
- expectTypeOf<EmployeeFields>().toHaveProperty('rating');
84
- });
85
-
86
- it('falls back to Record<string, unknown> for non-model types', () => {
87
- type NoFields = InferFields<{ slug: 'x' }>;
88
- expectTypeOf<NoFields>().toEqualTypeOf<Record<string, unknown>>();
89
- });
90
- });
91
-
92
- describe('InferTransitions', () => {
93
- it('extracts transition names as a union', () => {
94
- expectTypeOf<EmployeeTransitions>().toEqualTypeOf<
95
- 'activate' | 'request_leave' | 'return_from_leave' | 'terminate'
96
- >();
97
- });
98
-
99
- it('falls back to string for non-model types', () => {
100
- type NoTrans = InferTransitions<{ slug: 'x' }>;
101
- expectTypeOf<NoTrans>().toEqualTypeOf<string>();
102
- });
103
- });
104
-
105
- describe('InferStates', () => {
106
- it('extracts state names as a union', () => {
107
- expectTypeOf<EmployeeStates>().toEqualTypeOf<
108
- 'onboarding' | 'active' | 'on_leave' | 'terminated'
109
- >();
110
- });
111
-
112
- it('falls back to string for non-model types', () => {
113
- type NoStates = InferStates<{ slug: 'x' }>;
114
- expectTypeOf<NoStates>().toEqualTypeOf<string>();
115
- });
116
- });
117
-
118
- describe('InferSlug', () => {
119
- it('extracts the literal slug type', () => {
120
- expectTypeOf<InferSlug<typeof employeeModel>>().toEqualTypeOf<'employee'>();
121
- });
122
- });
123
-
124
- describe('InferFieldNames', () => {
125
- it('extracts field name union', () => {
126
- expectTypeOf<InferFieldNames<typeof employeeModel>>().toEqualTypeOf<
127
- 'name' | 'email' | 'salary' | 'isActive' | 'startDate' | 'tags' | 'metadata' | 'department' | 'rating'
128
- >();
129
- });
130
- });
131
-
132
- describe('defineModel', () => {
133
- it('returns the definition unchanged at runtime', () => {
134
- expect(employeeModel.slug).toBe('employee');
135
- expect(Object.keys(employeeModel.fields)).toHaveLength(9);
136
- expect(Object.keys(employeeModel.states)).toHaveLength(4);
137
- expect(Object.keys(employeeModel.transitions)).toHaveLength(4);
138
- });
139
-
140
- it('preserves const literal types', () => {
141
- // The slug should be the literal 'employee', not string
142
- expectTypeOf(employeeModel.slug).toEqualTypeOf<'employee'>();
143
- // Field keys should be literal union
144
- expectTypeOf<keyof typeof employeeModel.fields>().toEqualTypeOf<
145
- 'name' | 'email' | 'salary' | 'isActive' | 'startDate' | 'tags' | 'metadata' | 'department' | 'rating'
146
- >();
147
- });
148
-
149
- it('throws in dev mode when slug is missing', () => {
150
- expect(() => defineModel({ slug: '', fields: {}, states: {}, transitions: {} })).toThrow(
151
- 'slug is required',
152
- );
153
- });
154
-
155
- it('throws in dev mode when fields is missing', () => {
156
- expect(() => defineModel({ slug: 'x', fields: null as any, states: {}, transitions: {} })).toThrow(
157
- 'fields object is required',
158
- );
159
- });
160
- });