@mmapp/player-core 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +66 -0
  2. package/dist/index.d.mts +81 -1
  3. package/dist/index.d.ts +81 -1
  4. package/dist/index.js +81 -2
  5. package/dist/index.mjs +79 -2
  6. package/package.json +3 -2
  7. package/package.json.backup +0 -35
  8. package/src/__tests__/actions.test.ts +0 -187
  9. package/src/__tests__/blueprint-e2e.test.ts +0 -706
  10. package/src/__tests__/blueprint-test-runner.test.ts +0 -680
  11. package/src/__tests__/core-functions.test.ts +0 -78
  12. package/src/__tests__/dsl-compiler.test.ts +0 -1382
  13. package/src/__tests__/dsl-grammar.test.ts +0 -1682
  14. package/src/__tests__/events.test.ts +0 -200
  15. package/src/__tests__/expression.test.ts +0 -296
  16. package/src/__tests__/failure-policies.test.ts +0 -110
  17. package/src/__tests__/frontend-context.test.ts +0 -182
  18. package/src/__tests__/integration.test.ts +0 -256
  19. package/src/__tests__/security.test.ts +0 -190
  20. package/src/__tests__/state-machine.test.ts +0 -450
  21. package/src/__tests__/testing-engine.test.ts +0 -671
  22. package/src/actions/dispatcher.ts +0 -80
  23. package/src/actions/index.ts +0 -7
  24. package/src/actions/types.ts +0 -25
  25. package/src/dsl/compiler/component-mapper.ts +0 -289
  26. package/src/dsl/compiler/field-mapper.ts +0 -187
  27. package/src/dsl/compiler/index.ts +0 -82
  28. package/src/dsl/compiler/manifest-compiler.ts +0 -76
  29. package/src/dsl/compiler/symbol-table.ts +0 -214
  30. package/src/dsl/compiler/utils.ts +0 -48
  31. package/src/dsl/compiler/view-compiler.ts +0 -286
  32. package/src/dsl/compiler/workflow-compiler.ts +0 -600
  33. package/src/dsl/index.ts +0 -66
  34. package/src/dsl/ir-migration.ts +0 -221
  35. package/src/dsl/ir-types.ts +0 -416
  36. package/src/dsl/lexer.ts +0 -579
  37. package/src/dsl/parser.ts +0 -115
  38. package/src/dsl/types.ts +0 -256
  39. package/src/events/event-bus.ts +0 -68
  40. package/src/events/index.ts +0 -9
  41. package/src/events/pattern-matcher.ts +0 -61
  42. package/src/events/types.ts +0 -27
  43. package/src/expression/evaluator.ts +0 -676
  44. package/src/expression/functions.ts +0 -214
  45. package/src/expression/index.ts +0 -13
  46. package/src/expression/types.ts +0 -64
  47. package/src/index.ts +0 -61
  48. package/src/state-machine/index.ts +0 -16
  49. package/src/state-machine/interpreter.ts +0 -319
  50. package/src/state-machine/types.ts +0 -89
  51. package/src/testing/action-trace.ts +0 -209
  52. package/src/testing/blueprint-test-runner.ts +0 -214
  53. package/src/testing/graph-walker.ts +0 -249
  54. package/src/testing/index.ts +0 -69
  55. package/src/testing/nrt-comparator.ts +0 -199
  56. package/src/testing/nrt-types.ts +0 -230
  57. package/src/testing/test-actions.ts +0 -645
  58. package/src/testing/test-compiler.ts +0 -278
  59. package/src/testing/test-runner.ts +0 -444
  60. package/src/testing/types.ts +0 -231
  61. package/src/validation/definition-validator.ts +0 -812
  62. package/src/validation/index.ts +0 -13
  63. package/tsconfig.json +0 -26
  64. package/vitest.config.ts +0 -8
@@ -1,671 +0,0 @@
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
- });