@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,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint Test Runner — tests that the test IS a Blueprint running on a Player.
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - TestProgram → Blueprint compilation
|
|
6
|
+
* - In-process execution via Blueprint runner
|
|
7
|
+
* - API execution via mock adapter
|
|
8
|
+
* - Same TestProgram, different modes, same results
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { compileTestScenario, compileTestProgram } from '../testing/test-compiler';
|
|
13
|
+
import { runBlueprintTestProgram, runBlueprintScenario } from '../testing/blueprint-test-runner';
|
|
14
|
+
import type { PlayerWorkflowDefinition } from '../state-machine/types';
|
|
15
|
+
import type { TestProgram, TestScenario, ApiTestAdapter, ApiInstanceSnapshot } from '../testing/types';
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Fixtures: PM Blueprint definitions
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
const PM_TASK_DEFINITION: PlayerWorkflowDefinition = {
|
|
22
|
+
id: 'pm-task',
|
|
23
|
+
slug: 'pm-task',
|
|
24
|
+
states: [
|
|
25
|
+
{
|
|
26
|
+
name: 'todo',
|
|
27
|
+
type: 'START',
|
|
28
|
+
on_enter: [
|
|
29
|
+
{ type: 'set_field', config: { field: 'status_label', value: 'To Do' } },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'in_progress',
|
|
34
|
+
type: 'REGULAR',
|
|
35
|
+
on_enter: [
|
|
36
|
+
{ type: 'set_field', config: { field: 'status_label', value: 'In Progress' } },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'done',
|
|
41
|
+
type: 'END',
|
|
42
|
+
on_enter: [
|
|
43
|
+
{ type: 'set_field', config: { field: 'status_label', value: 'Done' } },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'cancelled',
|
|
48
|
+
type: 'CANCELLED',
|
|
49
|
+
on_enter: [
|
|
50
|
+
{ type: 'set_field', config: { field: 'status_label', value: 'Cancelled' } },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
transitions: [
|
|
55
|
+
{ name: 'start', from: ['todo'], to: 'in_progress' },
|
|
56
|
+
{ name: 'complete', from: ['in_progress'], to: 'done' },
|
|
57
|
+
{ name: 'cancel', from: ['todo', 'in_progress'], to: 'cancelled' },
|
|
58
|
+
{ name: 'reopen', from: ['done', 'cancelled'], to: 'todo' },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const PM_PROJECT_DEFINITION: PlayerWorkflowDefinition = {
|
|
63
|
+
id: 'pm-project',
|
|
64
|
+
slug: 'pm-project',
|
|
65
|
+
states: [
|
|
66
|
+
{
|
|
67
|
+
name: 'draft',
|
|
68
|
+
type: 'START',
|
|
69
|
+
on_enter: [
|
|
70
|
+
{ type: 'set_field', config: { field: 'total_tasks', value: 0 } },
|
|
71
|
+
{ type: 'set_field', config: { field: 'completed_tasks', value: 0 } },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'active',
|
|
76
|
+
type: 'REGULAR',
|
|
77
|
+
on_event: [
|
|
78
|
+
{
|
|
79
|
+
match: '*:*:pm-task:transition.completed',
|
|
80
|
+
actions: [
|
|
81
|
+
{
|
|
82
|
+
type: 'set_field',
|
|
83
|
+
config: { field: 'last_task_event', value: 'task_transitioned' },
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{ name: 'completed', type: 'END' },
|
|
90
|
+
{ name: 'cancelled', type: 'CANCELLED' },
|
|
91
|
+
],
|
|
92
|
+
transitions: [
|
|
93
|
+
{ name: 'activate', from: ['draft'], to: 'active' },
|
|
94
|
+
{
|
|
95
|
+
name: 'auto_complete',
|
|
96
|
+
from: ['active'],
|
|
97
|
+
to: 'completed',
|
|
98
|
+
auto: true,
|
|
99
|
+
conditions: ['gt(completed_tasks, 0)', 'eq(completed_tasks, total_tasks)'],
|
|
100
|
+
},
|
|
101
|
+
{ name: 'cancel', from: ['draft', 'active'], to: 'cancelled' },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// =============================================================================
|
|
106
|
+
// Compiler Tests
|
|
107
|
+
// =============================================================================
|
|
108
|
+
|
|
109
|
+
describe('Test Compiler', () => {
|
|
110
|
+
it('compiles a single-step scenario to a valid Blueprint', () => {
|
|
111
|
+
const scenario: TestScenario = {
|
|
112
|
+
name: 'Simple',
|
|
113
|
+
steps: [
|
|
114
|
+
{
|
|
115
|
+
name: 'Start task',
|
|
116
|
+
action: { type: 'transition', name: 'start' },
|
|
117
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'in_progress' }],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const blueprint = compileTestScenario(scenario, { 'pm-task': PM_TASK_DEFINITION });
|
|
123
|
+
|
|
124
|
+
// Has required states
|
|
125
|
+
const stateNames = blueprint.states.map(s => s.name);
|
|
126
|
+
expect(stateNames).toContain('__idle');
|
|
127
|
+
expect(stateNames).toContain('__setup');
|
|
128
|
+
expect(stateNames).toContain('__step_0');
|
|
129
|
+
expect(stateNames).toContain('__pass');
|
|
130
|
+
expect(stateNames).toContain('__fail');
|
|
131
|
+
|
|
132
|
+
// START state is __idle (empty, no actions)
|
|
133
|
+
const startState = blueprint.states.find(s => s.type === 'START');
|
|
134
|
+
expect(startState?.name).toBe('__idle');
|
|
135
|
+
|
|
136
|
+
// __setup is REGULAR with on_enter actions
|
|
137
|
+
const setupState = blueprint.states.find(s => s.name === '__setup');
|
|
138
|
+
expect(setupState?.type).toBe('REGULAR');
|
|
139
|
+
|
|
140
|
+
// Has transitions (including setup: __idle → __setup)
|
|
141
|
+
expect(blueprint.transitions.length).toBeGreaterThan(0);
|
|
142
|
+
expect(blueprint.transitions.find(t => t.name === 'setup')).toBeDefined();
|
|
143
|
+
|
|
144
|
+
// Setup has __test_init and __test_create_instance actions
|
|
145
|
+
const setupActions = setupState!.on_enter!;
|
|
146
|
+
expect(setupActions.find(a => a.type === '__test_init')).toBeDefined();
|
|
147
|
+
expect(setupActions.find(a => a.type === '__test_create_instance')).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('compiles multi-step scenario with correct step count', () => {
|
|
151
|
+
const scenario: TestScenario = {
|
|
152
|
+
name: 'Multi-step',
|
|
153
|
+
steps: [
|
|
154
|
+
{
|
|
155
|
+
name: 'Step 1',
|
|
156
|
+
action: { type: 'transition', name: 'start' },
|
|
157
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'in_progress' }],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'Step 2',
|
|
161
|
+
action: { type: 'transition', name: 'complete' },
|
|
162
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'done' }],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const blueprint = compileTestScenario(scenario, { 'pm-task': PM_TASK_DEFINITION });
|
|
168
|
+
|
|
169
|
+
// Should have __idle, __setup, __step_0, __step_1, __pass, __fail = 6 states
|
|
170
|
+
expect(blueprint.states).toHaveLength(6);
|
|
171
|
+
|
|
172
|
+
// __step_0 on_enter should have: step_begin, transition, settle, assert, step_end
|
|
173
|
+
const step0 = blueprint.states.find(s => s.name === '__step_0');
|
|
174
|
+
expect(step0!.on_enter!.find(a => a.type === '__test_transition')).toBeDefined();
|
|
175
|
+
expect(step0!.on_enter!.find(a => a.type === '__test_assert')).toBeDefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('compiles multi-instance scenario with separate create actions', () => {
|
|
179
|
+
const scenario: TestScenario = {
|
|
180
|
+
name: 'Multi-instance',
|
|
181
|
+
connectEventBuses: true,
|
|
182
|
+
instances: [
|
|
183
|
+
{ id: 'project', definitionSlug: 'pm-project', initialData: { total_tasks: 1 } },
|
|
184
|
+
{ id: 'task', definitionSlug: 'pm-task' },
|
|
185
|
+
],
|
|
186
|
+
steps: [],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const blueprint = compileTestScenario(scenario, {
|
|
190
|
+
'pm-project': PM_PROJECT_DEFINITION,
|
|
191
|
+
'pm-task': PM_TASK_DEFINITION,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const setup = blueprint.states.find(s => s.name === '__setup');
|
|
195
|
+
const createActions = setup!.on_enter!.filter(a => a.type === '__test_create_instance');
|
|
196
|
+
expect(createActions).toHaveLength(2);
|
|
197
|
+
expect(createActions[0].config.testId).toBe('project');
|
|
198
|
+
expect(createActions[1].config.testId).toBe('task');
|
|
199
|
+
|
|
200
|
+
// Init should have connectEventBuses
|
|
201
|
+
const initAction = setup!.on_enter!.find(a => a.type === '__test_init');
|
|
202
|
+
expect(initAction!.config.connectEventBuses).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('compileTestProgram produces one Blueprint per scenario', () => {
|
|
206
|
+
const program: TestProgram = {
|
|
207
|
+
name: 'Multi-scenario',
|
|
208
|
+
definitionSlug: 'pm-task',
|
|
209
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
210
|
+
scenarios: [
|
|
211
|
+
{ name: 'S1', steps: [] },
|
|
212
|
+
{ name: 'S2', steps: [] },
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const blueprints = compileTestProgram(program);
|
|
217
|
+
expect(blueprints).toHaveLength(2);
|
|
218
|
+
expect(blueprints[0].slug).toBe('__test_scenario_0');
|
|
219
|
+
expect(blueprints[1].slug).toBe('__test_scenario_1');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('step with assert_only has no test action, only assertions', () => {
|
|
223
|
+
const scenario: TestScenario = {
|
|
224
|
+
name: 'Assert only',
|
|
225
|
+
steps: [
|
|
226
|
+
{
|
|
227
|
+
name: 'Check state',
|
|
228
|
+
action: { type: 'assert_only' },
|
|
229
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'todo' }],
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const blueprint = compileTestScenario(scenario, { 'pm-task': PM_TASK_DEFINITION });
|
|
235
|
+
const step = blueprint.states.find(s => s.name === '__step_0');
|
|
236
|
+
const actions = step!.on_enter!;
|
|
237
|
+
|
|
238
|
+
// Should NOT have __test_transition, __test_set_field, etc.
|
|
239
|
+
expect(actions.find(a => a.type === '__test_transition')).toBeUndefined();
|
|
240
|
+
expect(actions.find(a => a.type === '__test_set_field')).toBeUndefined();
|
|
241
|
+
|
|
242
|
+
// Should HAVE __test_assert
|
|
243
|
+
expect(actions.find(a => a.type === '__test_assert')).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// =============================================================================
|
|
248
|
+
// Blueprint Runner — In-Process Mode
|
|
249
|
+
// =============================================================================
|
|
250
|
+
|
|
251
|
+
describe('Blueprint Runner (in-process)', () => {
|
|
252
|
+
it('runs PM-Task lifecycle through compiled Blueprint', async () => {
|
|
253
|
+
const program: TestProgram = {
|
|
254
|
+
name: 'PM-Task Lifecycle',
|
|
255
|
+
definitionSlug: 'pm-task',
|
|
256
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
257
|
+
scenarios: [
|
|
258
|
+
{
|
|
259
|
+
name: 'Happy path',
|
|
260
|
+
steps: [
|
|
261
|
+
{
|
|
262
|
+
name: 'Start task',
|
|
263
|
+
action: { type: 'transition', name: 'start' },
|
|
264
|
+
assertions: [
|
|
265
|
+
{ target: 'state', operator: 'eq', expected: 'in_progress' },
|
|
266
|
+
{ target: 'status', operator: 'eq', expected: 'ACTIVE' },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'Complete task',
|
|
271
|
+
action: { type: 'transition', name: 'complete' },
|
|
272
|
+
assertions: [
|
|
273
|
+
{ target: 'state', operator: 'eq', expected: 'done' },
|
|
274
|
+
{ target: 'status', operator: 'eq', expected: 'COMPLETED' },
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = await runBlueprintTestProgram(program, { mode: 'in-process' });
|
|
283
|
+
|
|
284
|
+
expect(result.passed).toBe(true);
|
|
285
|
+
expect(result.scenarioResults).toHaveLength(1);
|
|
286
|
+
expect(result.scenarioResults[0].passed).toBe(true);
|
|
287
|
+
expect(result.scenarioResults[0].stepResults).toHaveLength(2);
|
|
288
|
+
expect(result.durationMs).toBeLessThan(100);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('reports failing assertion', async () => {
|
|
292
|
+
const program: TestProgram = {
|
|
293
|
+
name: 'Failing',
|
|
294
|
+
definitionSlug: 'pm-task',
|
|
295
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
296
|
+
scenarios: [
|
|
297
|
+
{
|
|
298
|
+
name: 'Wrong state',
|
|
299
|
+
steps: [
|
|
300
|
+
{
|
|
301
|
+
name: 'Start then expect done',
|
|
302
|
+
action: { type: 'transition', name: 'start' },
|
|
303
|
+
assertions: [
|
|
304
|
+
{ target: 'state', operator: 'eq', expected: 'done' },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const result = await runBlueprintTestProgram(program, { mode: 'in-process' });
|
|
313
|
+
|
|
314
|
+
expect(result.passed).toBe(false);
|
|
315
|
+
const step = result.scenarioResults[0].stepResults[0];
|
|
316
|
+
expect(step.passed).toBe(false);
|
|
317
|
+
expect(step.assertionResults[0].actual).toBe('in_progress');
|
|
318
|
+
expect(step.assertionResults[0].assertion.expected).toBe('done');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('handles set_field action', async () => {
|
|
322
|
+
const program: TestProgram = {
|
|
323
|
+
name: 'Set field',
|
|
324
|
+
definitionSlug: 'pm-task',
|
|
325
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
326
|
+
scenarios: [
|
|
327
|
+
{
|
|
328
|
+
name: 'Set field',
|
|
329
|
+
steps: [
|
|
330
|
+
{
|
|
331
|
+
name: 'Set priority',
|
|
332
|
+
action: { type: 'set_field', field: 'priority', value: 'high' },
|
|
333
|
+
assertions: [
|
|
334
|
+
{ target: 'state_data', path: 'priority', operator: 'eq', expected: 'high' },
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const result = await runBlueprintTestProgram(program, { mode: 'in-process' });
|
|
343
|
+
expect(result.passed).toBe(true);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('handles assert_only step', async () => {
|
|
347
|
+
const program: TestProgram = {
|
|
348
|
+
name: 'Assert only',
|
|
349
|
+
definitionSlug: 'pm-task',
|
|
350
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
351
|
+
scenarios: [
|
|
352
|
+
{
|
|
353
|
+
name: 'Check initial state',
|
|
354
|
+
steps: [
|
|
355
|
+
{
|
|
356
|
+
name: 'Verify todo state',
|
|
357
|
+
action: { type: 'assert_only' },
|
|
358
|
+
assertions: [
|
|
359
|
+
{ target: 'state', operator: 'eq', expected: 'todo' },
|
|
360
|
+
{ target: 'available_transitions', operator: 'contains', expected: 'start' },
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const result = await runBlueprintTestProgram(program, { mode: 'in-process' });
|
|
369
|
+
expect(result.passed).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('multi-instance with EventBus cross-reactions', async () => {
|
|
373
|
+
const program: TestProgram = {
|
|
374
|
+
name: 'Multi-instance',
|
|
375
|
+
definitionSlug: 'pm-project',
|
|
376
|
+
definitions: {
|
|
377
|
+
'pm-project': PM_PROJECT_DEFINITION,
|
|
378
|
+
'pm-task': PM_TASK_DEFINITION,
|
|
379
|
+
},
|
|
380
|
+
scenarios: [
|
|
381
|
+
{
|
|
382
|
+
name: 'Task completion updates project',
|
|
383
|
+
connectEventBuses: true,
|
|
384
|
+
instances: [
|
|
385
|
+
{ id: 'project', definitionSlug: 'pm-project', initialData: { total_tasks: 2, completed_tasks: 0 } },
|
|
386
|
+
{ id: 'task1', definitionSlug: 'pm-task' },
|
|
387
|
+
],
|
|
388
|
+
steps: [
|
|
389
|
+
{
|
|
390
|
+
name: 'Activate project',
|
|
391
|
+
instanceId: 'project',
|
|
392
|
+
action: { type: 'transition', name: 'activate' },
|
|
393
|
+
assertions: [
|
|
394
|
+
{ target: 'state', instanceId: 'project', operator: 'eq', expected: 'active' },
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: 'Start task',
|
|
399
|
+
instanceId: 'task1',
|
|
400
|
+
action: { type: 'transition', name: 'start' },
|
|
401
|
+
assertions: [
|
|
402
|
+
{ target: 'state', instanceId: 'task1', operator: 'eq', expected: 'in_progress' },
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'Publish task completed event',
|
|
407
|
+
action: {
|
|
408
|
+
type: 'publish_event',
|
|
409
|
+
topic: 'stakeholder:s1:pm-task:transition.completed',
|
|
410
|
+
payload: { transition_name: 'complete' },
|
|
411
|
+
},
|
|
412
|
+
assertions: [
|
|
413
|
+
{
|
|
414
|
+
target: 'state_data',
|
|
415
|
+
instanceId: 'project',
|
|
416
|
+
path: 'last_task_event',
|
|
417
|
+
operator: 'eq',
|
|
418
|
+
expected: 'task_transitioned',
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const result = await runBlueprintTestProgram(program, { mode: 'in-process' });
|
|
428
|
+
|
|
429
|
+
expect(result.passed).toBe(true);
|
|
430
|
+
expect(result.scenarioResults[0].stepResults).toHaveLength(3);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('calls onStepComplete callback', async () => {
|
|
434
|
+
const onStepComplete = vi.fn();
|
|
435
|
+
|
|
436
|
+
const program: TestProgram = {
|
|
437
|
+
name: 'Callbacks',
|
|
438
|
+
definitionSlug: 'pm-task',
|
|
439
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
440
|
+
scenarios: [
|
|
441
|
+
{
|
|
442
|
+
name: 'Two steps',
|
|
443
|
+
steps: [
|
|
444
|
+
{
|
|
445
|
+
name: 'Start',
|
|
446
|
+
action: { type: 'transition', name: 'start' },
|
|
447
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'in_progress' }],
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: 'Complete',
|
|
451
|
+
action: { type: 'transition', name: 'complete' },
|
|
452
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'done' }],
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
await runBlueprintTestProgram(program, { mode: 'in-process', onStepComplete });
|
|
460
|
+
expect(onStepComplete).toHaveBeenCalledTimes(2);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('stops on first failed step', async () => {
|
|
464
|
+
const program: TestProgram = {
|
|
465
|
+
name: 'Fail early',
|
|
466
|
+
definitionSlug: 'pm-task',
|
|
467
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
468
|
+
scenarios: [
|
|
469
|
+
{
|
|
470
|
+
name: 'Fails at step 1',
|
|
471
|
+
steps: [
|
|
472
|
+
{
|
|
473
|
+
name: 'Wrong assertion',
|
|
474
|
+
action: { type: 'transition', name: 'start' },
|
|
475
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'done' }],
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: 'Should not run',
|
|
479
|
+
action: { type: 'transition', name: 'complete' },
|
|
480
|
+
assertions: [{ target: 'state', operator: 'eq', expected: 'done' }],
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const result = await runBlueprintTestProgram(program, { mode: 'in-process' });
|
|
488
|
+
expect(result.passed).toBe(false);
|
|
489
|
+
// Only one step should have executed
|
|
490
|
+
expect(result.scenarioResults[0].stepResults).toHaveLength(1);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// =============================================================================
|
|
495
|
+
// Blueprint Runner — API Mode (Mock Adapter)
|
|
496
|
+
// =============================================================================
|
|
497
|
+
|
|
498
|
+
describe('Blueprint Runner (API mode)', () => {
|
|
499
|
+
function createMockAdapter(): ApiTestAdapter & { instances: Map<string, any> } {
|
|
500
|
+
const instances = new Map<string, any>();
|
|
501
|
+
let nextId = 1;
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
instances,
|
|
505
|
+
|
|
506
|
+
async createInstance({ definitionSlug, stateData }) {
|
|
507
|
+
const id = `mock-${nextId++}`;
|
|
508
|
+
instances.set(id, {
|
|
509
|
+
id,
|
|
510
|
+
definitionSlug,
|
|
511
|
+
currentState: 'todo', // simplified: assume START state
|
|
512
|
+
status: 'PENDING',
|
|
513
|
+
stateData: stateData ?? {},
|
|
514
|
+
availableTransitions: ['start', 'cancel'],
|
|
515
|
+
});
|
|
516
|
+
return instances.get(id)!;
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
async startInstance(instanceId) {
|
|
520
|
+
const inst = instances.get(instanceId);
|
|
521
|
+
if (inst) inst.status = 'ACTIVE';
|
|
522
|
+
return inst;
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
async deleteInstance(instanceId) {
|
|
526
|
+
instances.delete(instanceId);
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
async triggerTransition(instanceId, transitionName) {
|
|
530
|
+
const inst = instances.get(instanceId);
|
|
531
|
+
if (!inst) return { success: false, fromState: '', toState: '', error: 'Not found' };
|
|
532
|
+
|
|
533
|
+
// Simplified transition logic for mock
|
|
534
|
+
const fromState = inst.currentState;
|
|
535
|
+
const transitions: Record<string, Record<string, string>> = {
|
|
536
|
+
todo: { start: 'in_progress', cancel: 'cancelled' },
|
|
537
|
+
in_progress: { complete: 'done', cancel: 'cancelled' },
|
|
538
|
+
done: { reopen: 'todo' },
|
|
539
|
+
cancelled: { reopen: 'todo' },
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const targetState = transitions[fromState]?.[transitionName];
|
|
543
|
+
if (!targetState) {
|
|
544
|
+
return { success: false, fromState, toState: fromState, error: `Invalid transition: ${transitionName} from ${fromState}` };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
inst.currentState = targetState;
|
|
548
|
+
if (targetState === 'done') inst.status = 'COMPLETED';
|
|
549
|
+
if (targetState === 'cancelled') inst.status = 'CANCELLED';
|
|
550
|
+
|
|
551
|
+
return { success: true, fromState, toState: targetState };
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
async updateStateData(instanceId, data) {
|
|
555
|
+
const inst = instances.get(instanceId);
|
|
556
|
+
if (inst) inst.stateData = { ...inst.stateData, ...data };
|
|
557
|
+
return inst;
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
async getInstance(instanceId) {
|
|
561
|
+
const inst = instances.get(instanceId);
|
|
562
|
+
if (!inst) throw new Error(`Instance not found: ${instanceId}`);
|
|
563
|
+
return inst;
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
it('runs PM-Task lifecycle via mock API adapter', async () => {
|
|
569
|
+
const adapter = createMockAdapter();
|
|
570
|
+
|
|
571
|
+
const program: TestProgram = {
|
|
572
|
+
name: 'API Test',
|
|
573
|
+
definitionSlug: 'pm-task',
|
|
574
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
575
|
+
scenarios: [
|
|
576
|
+
{
|
|
577
|
+
name: 'Happy path via API',
|
|
578
|
+
steps: [
|
|
579
|
+
{
|
|
580
|
+
name: 'Start task',
|
|
581
|
+
action: { type: 'transition', name: 'start' },
|
|
582
|
+
assertions: [
|
|
583
|
+
{ target: 'state', operator: 'eq', expected: 'in_progress' },
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: 'Complete task',
|
|
588
|
+
action: { type: 'transition', name: 'complete' },
|
|
589
|
+
assertions: [
|
|
590
|
+
{ target: 'state', operator: 'eq', expected: 'done' },
|
|
591
|
+
{ target: 'status', operator: 'eq', expected: 'COMPLETED' },
|
|
592
|
+
],
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const result = await runBlueprintTestProgram(program, {
|
|
600
|
+
mode: 'api',
|
|
601
|
+
adapter,
|
|
602
|
+
settleDelayMs: 0, // No settle delay for tests
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
expect(result.passed).toBe(true);
|
|
606
|
+
expect(result.scenarioResults[0].stepResults).toHaveLength(2);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('reports transition failure via API', async () => {
|
|
610
|
+
const adapter = createMockAdapter();
|
|
611
|
+
|
|
612
|
+
const program: TestProgram = {
|
|
613
|
+
name: 'API Fail',
|
|
614
|
+
definitionSlug: 'pm-task',
|
|
615
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
616
|
+
scenarios: [
|
|
617
|
+
{
|
|
618
|
+
name: 'Invalid transition',
|
|
619
|
+
steps: [
|
|
620
|
+
{
|
|
621
|
+
name: 'Complete from todo (invalid)',
|
|
622
|
+
action: { type: 'transition', name: 'complete' },
|
|
623
|
+
assertions: [],
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const result = await runBlueprintTestProgram(program, {
|
|
631
|
+
mode: 'api',
|
|
632
|
+
adapter,
|
|
633
|
+
settleDelayMs: 0,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
expect(result.passed).toBe(false);
|
|
637
|
+
expect(result.scenarioResults[0].stepResults[0].error).toContain('Invalid transition');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('same TestProgram works in both modes', async () => {
|
|
641
|
+
const program: TestProgram = {
|
|
642
|
+
name: 'Cross-mode',
|
|
643
|
+
definitionSlug: 'pm-task',
|
|
644
|
+
definitions: { 'pm-task': PM_TASK_DEFINITION },
|
|
645
|
+
scenarios: [
|
|
646
|
+
{
|
|
647
|
+
name: 'Start task',
|
|
648
|
+
steps: [
|
|
649
|
+
{
|
|
650
|
+
name: 'Start',
|
|
651
|
+
action: { type: 'transition', name: 'start' },
|
|
652
|
+
assertions: [
|
|
653
|
+
{ target: 'state', operator: 'eq', expected: 'in_progress' },
|
|
654
|
+
],
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// In-process
|
|
662
|
+
const inProcessResult = await runBlueprintTestProgram(program, { mode: 'in-process' });
|
|
663
|
+
|
|
664
|
+
// API
|
|
665
|
+
const adapter = createMockAdapter();
|
|
666
|
+
const apiResult = await runBlueprintTestProgram(program, {
|
|
667
|
+
mode: 'api',
|
|
668
|
+
adapter,
|
|
669
|
+
settleDelayMs: 0,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Both should pass
|
|
673
|
+
expect(inProcessResult.passed).toBe(true);
|
|
674
|
+
expect(apiResult.passed).toBe(true);
|
|
675
|
+
|
|
676
|
+
// Both should have same step count
|
|
677
|
+
expect(inProcessResult.scenarioResults[0].stepResults.length)
|
|
678
|
+
.toBe(apiResult.scenarioResults[0].stepResults.length);
|
|
679
|
+
});
|
|
680
|
+
});
|