@mmapp/player-core 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.
- package/dist/index.d.mts +1436 -0
- package/dist/index.d.ts +1436 -0
- package/dist/index.js +4828 -0
- package/dist/index.mjs +4762 -0
- package/package.json +35 -0
- package/package.json.backup +35 -0
- package/src/__tests__/actions.test.ts +187 -0
- package/src/__tests__/blueprint-e2e.test.ts +706 -0
- package/src/__tests__/blueprint-test-runner.test.ts +680 -0
- package/src/__tests__/core-functions.test.ts +78 -0
- package/src/__tests__/dsl-compiler.test.ts +1382 -0
- package/src/__tests__/dsl-grammar.test.ts +1682 -0
- package/src/__tests__/events.test.ts +200 -0
- package/src/__tests__/expression.test.ts +296 -0
- package/src/__tests__/failure-policies.test.ts +110 -0
- package/src/__tests__/frontend-context.test.ts +182 -0
- package/src/__tests__/integration.test.ts +256 -0
- package/src/__tests__/security.test.ts +190 -0
- package/src/__tests__/state-machine.test.ts +450 -0
- package/src/__tests__/testing-engine.test.ts +671 -0
- package/src/actions/dispatcher.ts +80 -0
- package/src/actions/index.ts +7 -0
- package/src/actions/types.ts +25 -0
- package/src/dsl/compiler/component-mapper.ts +289 -0
- package/src/dsl/compiler/field-mapper.ts +187 -0
- package/src/dsl/compiler/index.ts +82 -0
- package/src/dsl/compiler/manifest-compiler.ts +76 -0
- package/src/dsl/compiler/symbol-table.ts +214 -0
- package/src/dsl/compiler/utils.ts +48 -0
- package/src/dsl/compiler/view-compiler.ts +286 -0
- package/src/dsl/compiler/workflow-compiler.ts +600 -0
- package/src/dsl/index.ts +66 -0
- package/src/dsl/ir-migration.ts +221 -0
- package/src/dsl/ir-types.ts +416 -0
- package/src/dsl/lexer.ts +579 -0
- package/src/dsl/parser.ts +115 -0
- package/src/dsl/types.ts +256 -0
- package/src/events/event-bus.ts +68 -0
- package/src/events/index.ts +9 -0
- package/src/events/pattern-matcher.ts +61 -0
- package/src/events/types.ts +27 -0
- package/src/expression/evaluator.ts +676 -0
- package/src/expression/functions.ts +214 -0
- package/src/expression/index.ts +13 -0
- package/src/expression/types.ts +64 -0
- package/src/index.ts +61 -0
- package/src/state-machine/index.ts +16 -0
- package/src/state-machine/interpreter.ts +319 -0
- package/src/state-machine/types.ts +89 -0
- package/src/testing/action-trace.ts +209 -0
- package/src/testing/blueprint-test-runner.ts +214 -0
- package/src/testing/graph-walker.ts +249 -0
- package/src/testing/index.ts +69 -0
- package/src/testing/nrt-comparator.ts +199 -0
- package/src/testing/nrt-types.ts +230 -0
- package/src/testing/test-actions.ts +645 -0
- package/src/testing/test-compiler.ts +278 -0
- package/src/testing/test-runner.ts +444 -0
- package/src/testing/types.ts +231 -0
- package/src/validation/definition-validator.ts +812 -0
- package/src/validation/index.ts +13 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing OS — Unit tests for the testing engine.
|
|
3
|
+
*
|
|
4
|
+
* Uses PM Blueprint definitions (PM-Task, PM-Project) as fixtures.
|
|
5
|
+
* Tests graph walker, coverage generation, test runner (single + multi-instance).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
+
import { analyzeDefinition, generateCoverageScenarios } from '../testing/graph-walker';
|
|
10
|
+
import { runTestProgram, runScenario } from '../testing/test-runner';
|
|
11
|
+
import type { PlayerWorkflowDefinition } from '../state-machine/types';
|
|
12
|
+
import type { TestProgram, TestScenario } from '../testing/types';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Fixtures: PM Blueprint definitions (from blueprint-e2e.test.ts)
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
const PM_TASK_DEFINITION: PlayerWorkflowDefinition = {
|
|
19
|
+
id: 'pm-task',
|
|
20
|
+
slug: 'pm-task',
|
|
21
|
+
states: [
|
|
22
|
+
{
|
|
23
|
+
name: 'todo',
|
|
24
|
+
type: 'START',
|
|
25
|
+
on_enter: [
|
|
26
|
+
{ type: 'set_field', config: { field: 'status_label', value: 'To Do' } },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'in_progress',
|
|
31
|
+
type: 'REGULAR',
|
|
32
|
+
on_enter: [
|
|
33
|
+
{ type: 'set_field', config: { field: 'status_label', value: 'In Progress' } },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'done',
|
|
38
|
+
type: 'END',
|
|
39
|
+
on_enter: [
|
|
40
|
+
{ type: 'set_field', config: { field: 'status_label', value: 'Done' } },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'cancelled',
|
|
45
|
+
type: 'CANCELLED',
|
|
46
|
+
on_enter: [
|
|
47
|
+
{ type: 'set_field', config: { field: 'status_label', value: 'Cancelled' } },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
transitions: [
|
|
52
|
+
{ name: 'start', from: ['todo'], to: 'in_progress' },
|
|
53
|
+
{ name: 'complete', from: ['in_progress'], to: 'done' },
|
|
54
|
+
{ name: 'cancel', from: ['todo', 'in_progress'], to: 'cancelled' },
|
|
55
|
+
{ name: 'reopen', from: ['done', 'cancelled'], to: 'todo' },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const PM_PROJECT_DEFINITION: PlayerWorkflowDefinition = {
|
|
60
|
+
id: 'pm-project',
|
|
61
|
+
slug: 'pm-project',
|
|
62
|
+
states: [
|
|
63
|
+
{
|
|
64
|
+
name: 'draft',
|
|
65
|
+
type: 'START',
|
|
66
|
+
on_enter: [
|
|
67
|
+
{ type: 'set_field', config: { field: 'total_tasks', value: 0 } },
|
|
68
|
+
{ type: 'set_field', config: { field: 'completed_tasks', value: 0 } },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'active',
|
|
73
|
+
type: 'REGULAR',
|
|
74
|
+
on_event: [
|
|
75
|
+
{
|
|
76
|
+
match: '*:*:pm-task:transition.completed',
|
|
77
|
+
actions: [
|
|
78
|
+
{
|
|
79
|
+
type: 'set_field',
|
|
80
|
+
config: { field: 'last_task_event', value: 'task_transitioned' },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{ name: 'completed', type: 'END' },
|
|
87
|
+
{ name: 'cancelled', type: 'CANCELLED' },
|
|
88
|
+
],
|
|
89
|
+
transitions: [
|
|
90
|
+
{ name: 'activate', from: ['draft'], to: 'active' },
|
|
91
|
+
{
|
|
92
|
+
name: 'auto_complete',
|
|
93
|
+
from: ['active'],
|
|
94
|
+
to: 'completed',
|
|
95
|
+
auto: true,
|
|
96
|
+
conditions: ['gt(completed_tasks, 0)', 'eq(completed_tasks, total_tasks)'],
|
|
97
|
+
},
|
|
98
|
+
{ name: 'cancel', from: ['draft', 'active'], to: 'cancelled' },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Graph Walker Tests
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
describe('Graph Walker', () => {
|
|
107
|
+
describe('analyzeDefinition', () => {
|
|
108
|
+
it('correctly analyzes PM-Task (4 states, 4 transitions)', () => {
|
|
109
|
+
const analysis = analyzeDefinition(PM_TASK_DEFINITION);
|
|
110
|
+
|
|
111
|
+
expect(analysis.states).toHaveLength(4);
|
|
112
|
+
expect(analysis.summary.totalStates).toBe(4);
|
|
113
|
+
expect(analysis.summary.reachableStates).toBe(4);
|
|
114
|
+
expect(analysis.unreachableStates).toHaveLength(0);
|
|
115
|
+
expect(analysis.deadEndStates).toHaveLength(0);
|
|
116
|
+
|
|
117
|
+
// All states reachable
|
|
118
|
+
for (const state of analysis.states) {
|
|
119
|
+
expect(state.reachable).toBe(true);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('finds terminal paths in PM-Task', () => {
|
|
124
|
+
const analysis = analyzeDefinition(PM_TASK_DEFINITION);
|
|
125
|
+
|
|
126
|
+
// Paths: todo→in_progress→done, todo→cancelled, todo→in_progress→cancelled
|
|
127
|
+
expect(analysis.terminalPaths.length).toBeGreaterThanOrEqual(3);
|
|
128
|
+
|
|
129
|
+
// Each terminal path should end at 'done' or 'cancelled'
|
|
130
|
+
for (const path of analysis.terminalPaths) {
|
|
131
|
+
const lastState = path.states[path.states.length - 1];
|
|
132
|
+
expect(['done', 'cancelled']).toContain(lastState);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('detects cycles (reopen from done/cancelled)', () => {
|
|
137
|
+
const analysis = analyzeDefinition(PM_TASK_DEFINITION);
|
|
138
|
+
|
|
139
|
+
// reopen creates cycles: done→todo and cancelled→todo
|
|
140
|
+
expect(analysis.cycles.length).toBeGreaterThan(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('correctly analyzes PM-Project (auto-transition)', () => {
|
|
144
|
+
const analysis = analyzeDefinition(PM_PROJECT_DEFINITION);
|
|
145
|
+
|
|
146
|
+
expect(analysis.states).toHaveLength(4);
|
|
147
|
+
expect(analysis.summary.reachableStates).toBe(4);
|
|
148
|
+
|
|
149
|
+
// Should have edges for auto_complete
|
|
150
|
+
const autoEdges = analysis.edges.filter(e => e.auto);
|
|
151
|
+
expect(autoEdges.length).toBe(1);
|
|
152
|
+
expect(autoEdges[0].name).toBe('auto_complete');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('identifies unreachable states', () => {
|
|
156
|
+
const isolated: PlayerWorkflowDefinition = {
|
|
157
|
+
id: 'test-isolated',
|
|
158
|
+
slug: 'test-isolated',
|
|
159
|
+
states: [
|
|
160
|
+
{ name: 'start', type: 'START' },
|
|
161
|
+
{ name: 'middle', type: 'REGULAR' },
|
|
162
|
+
{ name: 'end', type: 'END' },
|
|
163
|
+
{ name: 'orphan', type: 'REGULAR' }, // unreachable
|
|
164
|
+
],
|
|
165
|
+
transitions: [
|
|
166
|
+
{ name: 'go', from: ['start'], to: 'middle' },
|
|
167
|
+
{ name: 'finish', from: ['middle'], to: 'end' },
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const analysis = analyzeDefinition(isolated);
|
|
172
|
+
expect(analysis.unreachableStates).toContain('orphan');
|
|
173
|
+
expect(analysis.summary.unreachableStates).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('identifies dead-end states', () => {
|
|
177
|
+
const deadEnd: PlayerWorkflowDefinition = {
|
|
178
|
+
id: 'test-deadend',
|
|
179
|
+
slug: 'test-deadend',
|
|
180
|
+
states: [
|
|
181
|
+
{ name: 'start', type: 'START' },
|
|
182
|
+
{ name: 'stuck', type: 'REGULAR' }, // no outgoing transitions
|
|
183
|
+
],
|
|
184
|
+
transitions: [
|
|
185
|
+
{ name: 'go', from: ['start'], to: 'stuck' },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const analysis = analyzeDefinition(deadEnd);
|
|
190
|
+
expect(analysis.deadEndStates).toContain('stuck');
|
|
191
|
+
expect(analysis.summary.deadEndStates).toBe(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('handles definition with no START state', () => {
|
|
195
|
+
const noStart: PlayerWorkflowDefinition = {
|
|
196
|
+
id: 'no-start',
|
|
197
|
+
slug: 'no-start',
|
|
198
|
+
states: [
|
|
199
|
+
{ name: 'a', type: 'REGULAR' },
|
|
200
|
+
{ name: 'b', type: 'END' },
|
|
201
|
+
],
|
|
202
|
+
transitions: [
|
|
203
|
+
{ name: 'go', from: ['a'], to: 'b' },
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const analysis = analyzeDefinition(noStart);
|
|
208
|
+
expect(analysis.terminalPaths).toHaveLength(0);
|
|
209
|
+
expect(analysis.unreachableStates).toHaveLength(2);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('generateCoverageScenarios', () => {
|
|
214
|
+
it('generates one scenario per terminal path for PM-Task', () => {
|
|
215
|
+
const analysis = analyzeDefinition(PM_TASK_DEFINITION);
|
|
216
|
+
const scenarios = generateCoverageScenarios(PM_TASK_DEFINITION, analysis);
|
|
217
|
+
|
|
218
|
+
expect(scenarios.length).toBe(analysis.terminalPaths.length);
|
|
219
|
+
|
|
220
|
+
// Each scenario should have the 'auto-generated' tag
|
|
221
|
+
for (const scenario of scenarios) {
|
|
222
|
+
expect(scenario.tags).toContain('auto-generated');
|
|
223
|
+
expect(scenario.steps.length).toBeGreaterThan(0);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('scenario steps assert correct state after each transition', () => {
|
|
228
|
+
const scenarios = generateCoverageScenarios(PM_TASK_DEFINITION);
|
|
229
|
+
|
|
230
|
+
// Find the happy path: todo → in_progress → done
|
|
231
|
+
const happyPath = scenarios.find(s => s.name.includes('done'));
|
|
232
|
+
expect(happyPath).toBeDefined();
|
|
233
|
+
|
|
234
|
+
// First step should be 'start' transition, assert state 'in_progress'
|
|
235
|
+
const firstStep = happyPath!.steps[0];
|
|
236
|
+
expect(firstStep.action).toEqual({ type: 'transition', name: 'start' });
|
|
237
|
+
expect(firstStep.assertions[0]).toMatchObject({
|
|
238
|
+
target: 'state',
|
|
239
|
+
operator: 'eq',
|
|
240
|
+
expected: 'in_progress',
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('generates scenarios for PM-Project including auto-transition path', () => {
|
|
245
|
+
const scenarios = generateCoverageScenarios(PM_PROJECT_DEFINITION);
|
|
246
|
+
|
|
247
|
+
// Should have paths through completed and cancelled
|
|
248
|
+
expect(scenarios.length).toBeGreaterThanOrEqual(2);
|
|
249
|
+
|
|
250
|
+
const pathNames = scenarios.map(s => s.name);
|
|
251
|
+
expect(pathNames.some(n => n.includes('completed'))).toBe(true);
|
|
252
|
+
expect(pathNames.some(n => n.includes('cancelled'))).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('terminal steps assert status', () => {
|
|
256
|
+
const scenarios = generateCoverageScenarios(PM_TASK_DEFINITION);
|
|
257
|
+
|
|
258
|
+
const donePath = scenarios.find(s => s.name.includes('done'));
|
|
259
|
+
expect(donePath).toBeDefined();
|
|
260
|
+
|
|
261
|
+
const lastStep = donePath!.steps[donePath!.steps.length - 1];
|
|
262
|
+
const statusAssertion = lastStep.assertions.find(a => a.target === 'status');
|
|
263
|
+
expect(statusAssertion).toBeDefined();
|
|
264
|
+
expect(statusAssertion!.expected).toBe('COMPLETED');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// =============================================================================
|
|
270
|
+
// Test Runner Tests
|
|
271
|
+
// =============================================================================
|
|
272
|
+
|
|
273
|
+
describe('Test Runner', () => {
|
|
274
|
+
describe('Single-instance scenarios', () => {
|
|
275
|
+
it('runs PM-Task full lifecycle (todo → in_progress → done)', async () => {
|
|
276
|
+
const program: TestProgram = {
|
|
277
|
+
name: 'PM-Task Lifecycle',
|
|
278
|
+
definitionSlug: 'pm-task',
|
|
279
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
280
|
+
scenarios: [
|
|
281
|
+
{
|
|
282
|
+
name: 'Happy path',
|
|
283
|
+
steps: [
|
|
284
|
+
{
|
|
285
|
+
name: 'Start task',
|
|
286
|
+
action: { type: 'transition', name: 'start' },
|
|
287
|
+
assertions: [
|
|
288
|
+
{ target: 'state', operator: 'eq', expected: 'in_progress' },
|
|
289
|
+
{ target: 'status', operator: 'eq', expected: 'ACTIVE' },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'Complete task',
|
|
294
|
+
action: { type: 'transition', name: 'complete' },
|
|
295
|
+
assertions: [
|
|
296
|
+
{ target: 'state', operator: 'eq', expected: 'done' },
|
|
297
|
+
{ target: 'status', operator: 'eq', expected: 'COMPLETED' },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const result = await runTestProgram(program);
|
|
306
|
+
|
|
307
|
+
expect(result.passed).toBe(true);
|
|
308
|
+
expect(result.scenarioResults).toHaveLength(1);
|
|
309
|
+
expect(result.scenarioResults[0].passed).toBe(true);
|
|
310
|
+
expect(result.scenarioResults[0].stepResults).toHaveLength(2);
|
|
311
|
+
expect(result.durationMs).toBeLessThan(100);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('verifies state_data via set_field action', async () => {
|
|
315
|
+
const program: TestProgram = {
|
|
316
|
+
name: 'State data test',
|
|
317
|
+
definitionSlug: 'pm-task',
|
|
318
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
319
|
+
scenarios: [
|
|
320
|
+
{
|
|
321
|
+
name: 'set_field works',
|
|
322
|
+
steps: [
|
|
323
|
+
{
|
|
324
|
+
name: 'Set priority',
|
|
325
|
+
action: { type: 'set_field', field: 'priority', value: 'high' },
|
|
326
|
+
assertions: [
|
|
327
|
+
{ target: 'state_data', path: 'priority', operator: 'eq', expected: 'high' },
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const result = await runTestProgram(program);
|
|
336
|
+
expect(result.passed).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('reports failing assertion correctly', async () => {
|
|
340
|
+
const program: TestProgram = {
|
|
341
|
+
name: 'Failing test',
|
|
342
|
+
definitionSlug: 'pm-task',
|
|
343
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
344
|
+
scenarios: [
|
|
345
|
+
{
|
|
346
|
+
name: 'Wrong state',
|
|
347
|
+
steps: [
|
|
348
|
+
{
|
|
349
|
+
name: 'Start task',
|
|
350
|
+
action: { type: 'transition', name: 'start' },
|
|
351
|
+
assertions: [
|
|
352
|
+
{ target: 'state', operator: 'eq', expected: 'done', label: 'Should be done (will fail)' },
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const result = await runTestProgram(program);
|
|
361
|
+
|
|
362
|
+
expect(result.passed).toBe(false);
|
|
363
|
+
expect(result.scenarioResults[0].passed).toBe(false);
|
|
364
|
+
|
|
365
|
+
const failedAssertion = result.scenarioResults[0].stepResults[0].assertionResults[0];
|
|
366
|
+
expect(failedAssertion.passed).toBe(false);
|
|
367
|
+
expect(failedAssertion.actual).toBe('in_progress');
|
|
368
|
+
expect(failedAssertion.assertion.expected).toBe('done');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('available_transitions assertion works', async () => {
|
|
372
|
+
const program: TestProgram = {
|
|
373
|
+
name: 'Available transitions',
|
|
374
|
+
definitionSlug: 'pm-task',
|
|
375
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
376
|
+
scenarios: [
|
|
377
|
+
{
|
|
378
|
+
name: 'Check available transitions',
|
|
379
|
+
steps: [
|
|
380
|
+
{
|
|
381
|
+
name: 'In todo state',
|
|
382
|
+
action: { type: 'assert_only' },
|
|
383
|
+
assertions: [
|
|
384
|
+
{ target: 'available_transitions', operator: 'contains', expected: 'start' },
|
|
385
|
+
{ target: 'available_transitions', operator: 'contains', expected: 'cancel' },
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const result = await runTestProgram(program);
|
|
394
|
+
expect(result.passed).toBe(true);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('numeric comparison operators work', async () => {
|
|
398
|
+
const program: TestProgram = {
|
|
399
|
+
name: 'Numeric operators',
|
|
400
|
+
definitionSlug: 'pm-task',
|
|
401
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
402
|
+
scenarios: [
|
|
403
|
+
{
|
|
404
|
+
name: 'Numeric assertions',
|
|
405
|
+
steps: [
|
|
406
|
+
{
|
|
407
|
+
name: 'Set count',
|
|
408
|
+
action: { type: 'set_field', field: 'count', value: 5 },
|
|
409
|
+
assertions: [
|
|
410
|
+
{ target: 'state_data', path: 'count', operator: 'gt', expected: 3 },
|
|
411
|
+
{ target: 'state_data', path: 'count', operator: 'gte', expected: 5 },
|
|
412
|
+
{ target: 'state_data', path: 'count', operator: 'lt', expected: 10 },
|
|
413
|
+
{ target: 'state_data', path: 'count', operator: 'lte', expected: 5 },
|
|
414
|
+
{ target: 'state_data', path: 'count', operator: 'neq', expected: 0 },
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const result = await runTestProgram(program);
|
|
423
|
+
expect(result.passed).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe('Multi-instance scenarios', () => {
|
|
428
|
+
it('PM-Project + PM-Task connected via EventBus: complete task → verify project state_data', async () => {
|
|
429
|
+
const scenario: TestScenario = {
|
|
430
|
+
name: 'Multi-instance: task completion updates project',
|
|
431
|
+
connectEventBuses: true,
|
|
432
|
+
instances: [
|
|
433
|
+
{
|
|
434
|
+
id: 'project',
|
|
435
|
+
definitionSlug: 'pm-project',
|
|
436
|
+
initialData: { total_tasks: 2, completed_tasks: 0 },
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: 'task1',
|
|
440
|
+
definitionSlug: 'pm-task',
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
steps: [
|
|
444
|
+
// Activate the project
|
|
445
|
+
{
|
|
446
|
+
name: 'Activate project',
|
|
447
|
+
instanceId: 'project',
|
|
448
|
+
action: { type: 'transition', name: 'activate' },
|
|
449
|
+
assertions: [
|
|
450
|
+
{ target: 'state', instanceId: 'project', operator: 'eq', expected: 'active' },
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
// Start task
|
|
454
|
+
{
|
|
455
|
+
name: 'Start task',
|
|
456
|
+
instanceId: 'task1',
|
|
457
|
+
action: { type: 'transition', name: 'start' },
|
|
458
|
+
assertions: [
|
|
459
|
+
{ target: 'state', instanceId: 'task1', operator: 'eq', expected: 'in_progress' },
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
// Simulate task completion domain event on the shared event bus
|
|
463
|
+
{
|
|
464
|
+
name: 'Publish task completed event',
|
|
465
|
+
action: {
|
|
466
|
+
type: 'publish_event',
|
|
467
|
+
topic: 'stakeholder:s1:pm-task:transition.completed',
|
|
468
|
+
payload: {
|
|
469
|
+
transition_name: 'complete',
|
|
470
|
+
from_state: 'in_progress',
|
|
471
|
+
to_state: 'done',
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
assertions: [
|
|
475
|
+
// Project's on_event should have set last_task_event
|
|
476
|
+
{
|
|
477
|
+
target: 'state_data',
|
|
478
|
+
instanceId: 'project',
|
|
479
|
+
path: 'last_task_event',
|
|
480
|
+
operator: 'eq',
|
|
481
|
+
expected: 'task_transitioned',
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const result = await runScenario(
|
|
489
|
+
scenario,
|
|
490
|
+
{ 'pm-project': PM_PROJECT_DEFINITION, 'pm-task': PM_TASK_DEFINITION },
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
expect(result.passed).toBe(true);
|
|
494
|
+
expect(result.stepResults).toHaveLength(3);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe('Auto-generated coverage', () => {
|
|
499
|
+
it('runs auto-generated scenarios from graph walker', async () => {
|
|
500
|
+
const analysis = analyzeDefinition(PM_TASK_DEFINITION);
|
|
501
|
+
const scenarios = generateCoverageScenarios(PM_TASK_DEFINITION, analysis);
|
|
502
|
+
|
|
503
|
+
const program: TestProgram = {
|
|
504
|
+
name: 'Auto-generated PM-Task coverage',
|
|
505
|
+
definitionSlug: 'pm-task',
|
|
506
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
507
|
+
scenarios,
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const result = await runTestProgram(program);
|
|
511
|
+
|
|
512
|
+
// All auto-generated scenarios should pass
|
|
513
|
+
expect(result.passed).toBe(true);
|
|
514
|
+
expect(result.scenarioResults.length).toBe(scenarios.length);
|
|
515
|
+
expect(result.durationMs).toBeLessThan(100);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('Callbacks and abort', () => {
|
|
520
|
+
it('calls onStepComplete callback', async () => {
|
|
521
|
+
const onStepComplete = vi.fn();
|
|
522
|
+
|
|
523
|
+
const program: TestProgram = {
|
|
524
|
+
name: 'Callback test',
|
|
525
|
+
definitionSlug: 'pm-task',
|
|
526
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
527
|
+
scenarios: [
|
|
528
|
+
{
|
|
529
|
+
name: 'With callbacks',
|
|
530
|
+
steps: [
|
|
531
|
+
{
|
|
532
|
+
name: 'Start',
|
|
533
|
+
action: { type: 'transition', name: 'start' },
|
|
534
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'in_progress' }],
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: 'Complete',
|
|
538
|
+
action: { type: 'transition', name: 'complete' },
|
|
539
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'done' }],
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
await runTestProgram(program, { onStepComplete });
|
|
547
|
+
|
|
548
|
+
expect(onStepComplete).toHaveBeenCalledTimes(2);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('calls onScenarioComplete callback', async () => {
|
|
552
|
+
const onScenarioComplete = vi.fn();
|
|
553
|
+
|
|
554
|
+
const program: TestProgram = {
|
|
555
|
+
name: 'Scenario callback test',
|
|
556
|
+
definitionSlug: 'pm-task',
|
|
557
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
558
|
+
scenarios: [
|
|
559
|
+
{
|
|
560
|
+
name: 'First',
|
|
561
|
+
steps: [
|
|
562
|
+
{
|
|
563
|
+
name: 'Start',
|
|
564
|
+
action: { type: 'transition', name: 'start' },
|
|
565
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'in_progress' }],
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: 'Second',
|
|
571
|
+
steps: [
|
|
572
|
+
{
|
|
573
|
+
name: 'Cancel',
|
|
574
|
+
action: { type: 'transition', name: 'cancel' },
|
|
575
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'cancelled' }],
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
await runTestProgram(program, { onScenarioComplete });
|
|
583
|
+
|
|
584
|
+
expect(onScenarioComplete).toHaveBeenCalledTimes(2);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('abort signal stops execution', async () => {
|
|
588
|
+
const controller = new AbortController();
|
|
589
|
+
const onScenarioComplete = vi.fn();
|
|
590
|
+
|
|
591
|
+
// Abort after first scenario
|
|
592
|
+
onScenarioComplete.mockImplementation(() => {
|
|
593
|
+
controller.abort();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const program: TestProgram = {
|
|
597
|
+
name: 'Abort test',
|
|
598
|
+
definitionSlug: 'pm-task',
|
|
599
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
600
|
+
scenarios: [
|
|
601
|
+
{
|
|
602
|
+
name: 'First',
|
|
603
|
+
steps: [
|
|
604
|
+
{
|
|
605
|
+
name: 'Start',
|
|
606
|
+
action: { type: 'transition', name: 'start' },
|
|
607
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'in_progress' }],
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: 'Second (should be skipped)',
|
|
613
|
+
steps: [
|
|
614
|
+
{
|
|
615
|
+
name: 'Complete',
|
|
616
|
+
action: { type: 'transition', name: 'start' },
|
|
617
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'in_progress' }],
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const result = await runTestProgram(program, {
|
|
625
|
+
abortSignal: controller.signal,
|
|
626
|
+
onScenarioComplete,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Only first scenario should have run
|
|
630
|
+
expect(result.scenarioResults).toHaveLength(1);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
describe('Error handling', () => {
|
|
635
|
+
it('reports missing definition error', async () => {
|
|
636
|
+
const scenario: TestScenario = {
|
|
637
|
+
name: 'Missing def',
|
|
638
|
+
instances: [{ id: 'x', definitionSlug: 'nonexistent' }],
|
|
639
|
+
steps: [],
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const result = await runScenario(scenario, {});
|
|
643
|
+
expect(result.passed).toBe(false);
|
|
644
|
+
expect(result.error).toContain('nonexistent');
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('reports transition failure as step error', async () => {
|
|
648
|
+
const program: TestProgram = {
|
|
649
|
+
name: 'Bad transition',
|
|
650
|
+
definitionSlug: 'pm-task',
|
|
651
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
652
|
+
scenarios: [
|
|
653
|
+
{
|
|
654
|
+
name: 'Invalid transition',
|
|
655
|
+
steps: [
|
|
656
|
+
{
|
|
657
|
+
name: 'Complete from todo (invalid)',
|
|
658
|
+
action: { type: 'transition', name: 'complete' },
|
|
659
|
+
assertions: [],
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
},
|
|
663
|
+
],
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const result = await runTestProgram(program);
|
|
667
|
+
expect(result.passed).toBe(false);
|
|
668
|
+
expect(result.scenarioResults[0].stepResults[0].error).toContain('not valid');
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
});
|