@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.
- package/README.md +112 -0
- package/dist/index.d.mts +1378 -94
- package/dist/index.d.ts +1378 -94
- package/dist/index.js +1094 -1309
- package/dist/index.mjs +1038 -1296
- package/package.json +4 -3
- package/package.json.backup +0 -41
- package/src/Blueprint.ts +0 -216
- package/src/__tests__/Blueprint.test.ts +0 -106
- package/src/__tests__/action-context.test.ts +0 -166
- package/src/__tests__/actionCreators.test.ts +0 -179
- package/src/__tests__/builders.test.ts +0 -336
- package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
- package/src/__tests__/factories.test.ts +0 -229
- package/src/__tests__/loader.test.ts +0 -159
- package/src/__tests__/logger.test.ts +0 -70
- package/src/__tests__/type-inference.test.ts +0 -160
- package/src/__tests__/typed-transitions.test.ts +0 -126
- package/src/__tests__/useModuleConfig.test.ts +0 -61
- package/src/actionCreators.ts +0 -132
- package/src/actions.ts +0 -547
- package/src/atoms/index.ts +0 -600
- package/src/authoring.ts +0 -92
- package/src/browser-player.ts +0 -783
- package/src/builders.ts +0 -1342
- package/src/components/ExperienceWorkflowBridge.tsx +0 -123
- package/src/components/PlayerProvider.tsx +0 -43
- package/src/components/atoms/index.tsx +0 -269
- package/src/components/index.ts +0 -36
- package/src/conditions.ts +0 -692
- package/src/config/defineBlueprint.ts +0 -329
- package/src/config/defineModel.ts +0 -753
- package/src/config/defineWorkspace.ts +0 -24
- package/src/core/WorkflowRuntime.ts +0 -153
- package/src/factories.ts +0 -425
- package/src/grammar/index.ts +0 -173
- package/src/hooks/index.ts +0 -106
- package/src/hooks/useAuth.ts +0 -288
- package/src/hooks/useChannel.ts +0 -304
- package/src/hooks/useComputed.ts +0 -154
- package/src/hooks/useDomainSubscription.ts +0 -110
- package/src/hooks/useDuringAction.ts +0 -99
- package/src/hooks/useExperienceState.ts +0 -59
- package/src/hooks/useExpressionLibrary.ts +0 -129
- package/src/hooks/useForm.ts +0 -352
- package/src/hooks/useGeolocation.ts +0 -207
- package/src/hooks/useMapView.ts +0 -259
- package/src/hooks/useMiddleware.ts +0 -291
- package/src/hooks/useModel.ts +0 -363
- package/src/hooks/useModule.ts +0 -59
- package/src/hooks/useModuleConfig.ts +0 -61
- package/src/hooks/useMutation.ts +0 -237
- package/src/hooks/useNotification.ts +0 -151
- package/src/hooks/useOnChange.ts +0 -30
- package/src/hooks/useOnEnter.ts +0 -59
- package/src/hooks/useOnEvent.ts +0 -37
- package/src/hooks/useOnExit.ts +0 -27
- package/src/hooks/useOnTransition.ts +0 -30
- package/src/hooks/usePackage.ts +0 -128
- package/src/hooks/useParams.ts +0 -33
- package/src/hooks/usePlayer.ts +0 -308
- package/src/hooks/useQuery.ts +0 -184
- package/src/hooks/useRealtimeQuery.ts +0 -222
- package/src/hooks/useRole.ts +0 -191
- package/src/hooks/useRouteParams.ts +0 -100
- package/src/hooks/useRouter.ts +0 -347
- package/src/hooks/useServerAction.ts +0 -178
- package/src/hooks/useServerState.ts +0 -284
- package/src/hooks/useToast.ts +0 -164
- package/src/hooks/useTransition.ts +0 -39
- package/src/hooks/useView.ts +0 -102
- package/src/hooks/useWhileIn.ts +0 -48
- package/src/hooks/useWorkflow.ts +0 -63
- package/src/index.ts +0 -465
- package/src/loader/experience-workflow-loader.ts +0 -192
- package/src/loader/index.ts +0 -6
- package/src/local/LocalEngine.ts +0 -388
- package/src/local/LocalEngineAdapter.ts +0 -175
- package/src/local/LocalEngineContext.ts +0 -30
- package/src/logger.ts +0 -37
- package/src/mixins.ts +0 -1160
- package/src/providers/RuntimeContext.ts +0 -20
- package/src/providers/WorkflowProvider.tsx +0 -28
- package/src/routing/instance-key.ts +0 -107
- package/src/server/transition-context.ts +0 -172
- package/src/testing/index.ts +0 -9
- package/src/testing/useBlueprintTestRunner.ts +0 -91
- package/src/testing/useGraphAnalysis.ts +0 -18
- package/src/testing/useTestRunner.ts +0 -77
- package/src/testing.ts +0 -995
- package/src/types/workflow-inference.ts +0 -158
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -27
- 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
|
-
});
|