@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,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Machine Types — browser-side workflow interpretation.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the backend PureWorkflowDefinition/Instance structure
|
|
5
|
+
* but stripped to only what the browser engine needs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExpressionContext } from '../expression/types';
|
|
9
|
+
|
|
10
|
+
/** State type in the workflow */
|
|
11
|
+
export type StateType = 'START' | 'REGULAR' | 'END' | 'CANCELLED';
|
|
12
|
+
|
|
13
|
+
/** Action that runs during on_event handling */
|
|
14
|
+
export interface PlayerAction {
|
|
15
|
+
type: string;
|
|
16
|
+
config: Record<string, unknown>;
|
|
17
|
+
condition?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** On-event subscription for browser-side processing */
|
|
21
|
+
export interface PlayerOnEventSubscription {
|
|
22
|
+
match: string; // Topic pattern with wildcards
|
|
23
|
+
conditions?: string[];
|
|
24
|
+
actions: PlayerAction[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** State definition for the browser engine */
|
|
28
|
+
export interface PlayerStateDefinition {
|
|
29
|
+
name: string;
|
|
30
|
+
type: StateType;
|
|
31
|
+
on_enter?: PlayerAction[];
|
|
32
|
+
on_exit?: PlayerAction[];
|
|
33
|
+
on_event?: PlayerOnEventSubscription[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Transition definition */
|
|
37
|
+
export interface PlayerTransitionDefinition {
|
|
38
|
+
name: string;
|
|
39
|
+
from: string[];
|
|
40
|
+
to: string;
|
|
41
|
+
conditions?: string[];
|
|
42
|
+
auto?: boolean;
|
|
43
|
+
actions?: PlayerAction[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Workflow definition for the browser engine */
|
|
47
|
+
export interface PlayerWorkflowDefinition {
|
|
48
|
+
id: string;
|
|
49
|
+
slug: string;
|
|
50
|
+
states: PlayerStateDefinition[];
|
|
51
|
+
transitions: PlayerTransitionDefinition[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Runtime instance state */
|
|
55
|
+
export interface PlayerInstance {
|
|
56
|
+
definition: PlayerWorkflowDefinition;
|
|
57
|
+
current_state: string;
|
|
58
|
+
state_data: Record<string, unknown>;
|
|
59
|
+
memory: Record<string, unknown>;
|
|
60
|
+
status: 'ACTIVE' | 'COMPLETED' | 'CANCELLED';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Transition result from the state machine */
|
|
64
|
+
export interface TransitionResult {
|
|
65
|
+
success: boolean;
|
|
66
|
+
from_state: string;
|
|
67
|
+
to_state: string;
|
|
68
|
+
actions_executed: PlayerAction[];
|
|
69
|
+
error?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** State machine event listener */
|
|
73
|
+
export type StateMachineListener = (event: StateMachineEvent) => void;
|
|
74
|
+
|
|
75
|
+
/** Events emitted by the state machine */
|
|
76
|
+
export interface StateMachineEvent {
|
|
77
|
+
type: 'transition' | 'state_enter' | 'state_exit' | 'action_executed' | 'error';
|
|
78
|
+
instance_id: string;
|
|
79
|
+
from_state?: string;
|
|
80
|
+
to_state?: string;
|
|
81
|
+
action?: PlayerAction;
|
|
82
|
+
error?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Action handler registered with the state machine */
|
|
86
|
+
export type ActionHandler = (
|
|
87
|
+
action: PlayerAction,
|
|
88
|
+
context: ExpressionContext,
|
|
89
|
+
) => void | Promise<void>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Trace — records all workflow actions during execution.
|
|
3
|
+
*
|
|
4
|
+
* Captures a chronological trace of every transition, action, timer,
|
|
5
|
+
* and query that occurs during a workflow session. This enables:
|
|
6
|
+
*
|
|
7
|
+
* 1. Replay debugging (step through recorded actions)
|
|
8
|
+
* 2. Test assertions on action sequences
|
|
9
|
+
* 3. Performance profiling (timing of each tick)
|
|
10
|
+
* 4. Compliance auditing (complete action log)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const recorder = createActionRecorder();
|
|
14
|
+
* // ... hook into workflow runtime ...
|
|
15
|
+
* const trace = recorder.getTrace();
|
|
16
|
+
* expect(trace.ticks).toHaveLength(3);
|
|
17
|
+
* expect(trace.ticks[0].kind).toBe('transition');
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A complete action trace for a workflow session.
|
|
26
|
+
*/
|
|
27
|
+
export interface ActionTrace {
|
|
28
|
+
/** Trace format version. */
|
|
29
|
+
version: 1;
|
|
30
|
+
/** Workflow definition slug. */
|
|
31
|
+
workflowSlug: string;
|
|
32
|
+
/** Instance ID (if tracking a specific instance). */
|
|
33
|
+
instanceId?: string;
|
|
34
|
+
/** When the trace started. */
|
|
35
|
+
startedAt: string;
|
|
36
|
+
/** When the trace ended (if finalized). */
|
|
37
|
+
endedAt?: string;
|
|
38
|
+
/** Ordered list of action ticks. */
|
|
39
|
+
ticks: ActionTick[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A single action tick in the trace.
|
|
44
|
+
*/
|
|
45
|
+
export interface ActionTick {
|
|
46
|
+
/** Monotonically increasing tick number. */
|
|
47
|
+
tick: number;
|
|
48
|
+
/** ISO timestamp of the tick. */
|
|
49
|
+
timestamp: string;
|
|
50
|
+
/** Kind of action. */
|
|
51
|
+
kind: 'transition' | 'action' | 'timer' | 'query' | 'mutation' | 'event' | 'error';
|
|
52
|
+
/** Name/identifier of the action. */
|
|
53
|
+
name: string;
|
|
54
|
+
/** Duration in milliseconds (if measurable). */
|
|
55
|
+
durationMs?: number;
|
|
56
|
+
/** Workflow state before this tick. */
|
|
57
|
+
fromState?: string;
|
|
58
|
+
/** Workflow state after this tick. */
|
|
59
|
+
toState?: string;
|
|
60
|
+
/** Detailed information about the tick. */
|
|
61
|
+
detail: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// Recorder
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Action recorder — accumulates ticks into a trace.
|
|
70
|
+
*/
|
|
71
|
+
export interface ActionRecorder {
|
|
72
|
+
/** Record a transition. */
|
|
73
|
+
recordTransition: (name: string, fromState: string, toState: string, detail?: Record<string, unknown>) => void;
|
|
74
|
+
/** Record an action execution. */
|
|
75
|
+
recordAction: (name: string, detail?: Record<string, unknown>) => void;
|
|
76
|
+
/** Record a timer event. */
|
|
77
|
+
recordTimer: (name: string, detail?: Record<string, unknown>) => void;
|
|
78
|
+
/** Record a data query. */
|
|
79
|
+
recordQuery: (name: string, detail?: Record<string, unknown>) => void;
|
|
80
|
+
/** Record a mutation. */
|
|
81
|
+
recordMutation: (name: string, detail?: Record<string, unknown>) => void;
|
|
82
|
+
/** Record an event. */
|
|
83
|
+
recordEvent: (name: string, detail?: Record<string, unknown>) => void;
|
|
84
|
+
/** Record an error. */
|
|
85
|
+
recordError: (name: string, detail?: Record<string, unknown>) => void;
|
|
86
|
+
/** Get the current trace. */
|
|
87
|
+
getTrace: () => ActionTrace;
|
|
88
|
+
/** Finalize the trace (sets endedAt). */
|
|
89
|
+
finalize: () => ActionTrace;
|
|
90
|
+
/** Reset the recorder. */
|
|
91
|
+
reset: () => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a new action recorder.
|
|
96
|
+
*/
|
|
97
|
+
export function createActionRecorder(
|
|
98
|
+
workflowSlug: string,
|
|
99
|
+
instanceId?: string
|
|
100
|
+
): ActionRecorder {
|
|
101
|
+
let tickCounter = 0;
|
|
102
|
+
let ticks: ActionTick[] = [];
|
|
103
|
+
let startedAt = new Date().toISOString();
|
|
104
|
+
let endedAt: string | undefined;
|
|
105
|
+
|
|
106
|
+
function recordTick(kind: ActionTick['kind'], name: string, extra?: {
|
|
107
|
+
fromState?: string;
|
|
108
|
+
toState?: string;
|
|
109
|
+
durationMs?: number;
|
|
110
|
+
detail?: Record<string, unknown>;
|
|
111
|
+
}): void {
|
|
112
|
+
ticks.push({
|
|
113
|
+
tick: tickCounter++,
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
kind,
|
|
116
|
+
name,
|
|
117
|
+
fromState: extra?.fromState,
|
|
118
|
+
toState: extra?.toState,
|
|
119
|
+
durationMs: extra?.durationMs,
|
|
120
|
+
detail: extra?.detail || {},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
recordTransition(name, fromState, toState, detail) {
|
|
126
|
+
recordTick('transition', name, { fromState, toState, detail });
|
|
127
|
+
},
|
|
128
|
+
recordAction(name, detail) {
|
|
129
|
+
recordTick('action', name, { detail });
|
|
130
|
+
},
|
|
131
|
+
recordTimer(name, detail) {
|
|
132
|
+
recordTick('timer', name, { detail });
|
|
133
|
+
},
|
|
134
|
+
recordQuery(name, detail) {
|
|
135
|
+
recordTick('query', name, { detail });
|
|
136
|
+
},
|
|
137
|
+
recordMutation(name, detail) {
|
|
138
|
+
recordTick('mutation', name, { detail });
|
|
139
|
+
},
|
|
140
|
+
recordEvent(name, detail) {
|
|
141
|
+
recordTick('event', name, { detail });
|
|
142
|
+
},
|
|
143
|
+
recordError(name, detail) {
|
|
144
|
+
recordTick('error', name, { detail });
|
|
145
|
+
},
|
|
146
|
+
getTrace() {
|
|
147
|
+
return {
|
|
148
|
+
version: 1,
|
|
149
|
+
workflowSlug,
|
|
150
|
+
instanceId,
|
|
151
|
+
startedAt,
|
|
152
|
+
endedAt,
|
|
153
|
+
ticks: [...ticks],
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
finalize() {
|
|
157
|
+
endedAt = new Date().toISOString();
|
|
158
|
+
return this.getTrace();
|
|
159
|
+
},
|
|
160
|
+
reset() {
|
|
161
|
+
tickCounter = 0;
|
|
162
|
+
ticks = [];
|
|
163
|
+
startedAt = new Date().toISOString();
|
|
164
|
+
endedAt = undefined;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// =============================================================================
|
|
170
|
+
// Analysis utilities
|
|
171
|
+
// =============================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Filters a trace to only transition ticks.
|
|
175
|
+
*/
|
|
176
|
+
export function getTransitionPath(trace: ActionTrace): string[] {
|
|
177
|
+
return trace.ticks
|
|
178
|
+
.filter((t) => t.kind === 'transition')
|
|
179
|
+
.map((t) => `${t.fromState} → ${t.toState}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Gets the final state from a trace.
|
|
184
|
+
*/
|
|
185
|
+
export function getFinalState(trace: ActionTrace): string | undefined {
|
|
186
|
+
const transitions = trace.ticks.filter((t) => t.kind === 'transition');
|
|
187
|
+
if (transitions.length === 0) return undefined;
|
|
188
|
+
return transitions[transitions.length - 1].toState;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Counts ticks by kind.
|
|
193
|
+
*/
|
|
194
|
+
export function countByKind(trace: ActionTrace): Record<string, number> {
|
|
195
|
+
const counts: Record<string, number> = {};
|
|
196
|
+
for (const tick of trace.ticks) {
|
|
197
|
+
counts[tick.kind] = (counts[tick.kind] || 0) + 1;
|
|
198
|
+
}
|
|
199
|
+
return counts;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Checks if a trace contains a specific transition.
|
|
204
|
+
*/
|
|
205
|
+
export function hasTransition(trace: ActionTrace, from: string, to: string): boolean {
|
|
206
|
+
return trace.ticks.some(
|
|
207
|
+
(t) => t.kind === 'transition' && t.fromState === from && t.toState === to
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint Test Runner — executes test programs AS Blueprints on a Player.
|
|
3
|
+
*
|
|
4
|
+
* The test orchestrator IS a Player. This runner:
|
|
5
|
+
* 1. Compiles TestProgram → PlayerWorkflowDefinition (Blueprint)
|
|
6
|
+
* 2. Creates a StateMachine (Player) with the compiled Blueprint
|
|
7
|
+
* 3. Registers test action handlers (in-process or API)
|
|
8
|
+
* 4. Drives the StateMachine through transitions: next, next, next...
|
|
9
|
+
* 5. Collects results from the action handler state
|
|
10
|
+
*
|
|
11
|
+
* The compiled Blueprint can also be extracted, stored, and executed
|
|
12
|
+
* independently on any Player (browser, iOS, server).
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* - runBlueprintTestProgram(): Full program execution
|
|
16
|
+
* - runBlueprintScenario(): Single scenario execution
|
|
17
|
+
* - Supports both in-process and API modes via action handler factory
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { StateMachine } from '../state-machine/interpreter';
|
|
21
|
+
import { createEvaluator, WEB_FAILURE_POLICIES } from '../expression';
|
|
22
|
+
import { compileTestScenario } from './test-compiler';
|
|
23
|
+
import { createInProcessTestActions, createApiTestActions } from './test-actions';
|
|
24
|
+
import type { TestActionState } from './test-actions';
|
|
25
|
+
import type { PlayerWorkflowDefinition, ActionHandler } from '../state-machine/types';
|
|
26
|
+
import type {
|
|
27
|
+
TestProgram,
|
|
28
|
+
TestScenario,
|
|
29
|
+
TestProgramResult,
|
|
30
|
+
ScenarioResult,
|
|
31
|
+
StepResult,
|
|
32
|
+
ApiTestAdapter,
|
|
33
|
+
} from './types';
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Configuration
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
interface BlueprintTestConfig {
|
|
40
|
+
onStepComplete?: (scenarioIdx: number, step: StepResult) => void;
|
|
41
|
+
onScenarioComplete?: (result: ScenarioResult) => void;
|
|
42
|
+
abortSignal?: AbortSignal;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface InProcessConfig extends BlueprintTestConfig {
|
|
46
|
+
mode: 'in-process';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ApiConfig extends BlueprintTestConfig {
|
|
50
|
+
mode: 'api';
|
|
51
|
+
adapter: ApiTestAdapter;
|
|
52
|
+
settleDelayMs?: number;
|
|
53
|
+
cleanupInstances?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type BlueprintRunnerConfig = InProcessConfig | ApiConfig;
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// Public API
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Execute a complete test program using the Blueprint runner.
|
|
64
|
+
*
|
|
65
|
+
* The test program is compiled to Blueprints and executed on a Player
|
|
66
|
+
* (StateMachine) with the appropriate action handlers for the mode.
|
|
67
|
+
*/
|
|
68
|
+
export async function runBlueprintTestProgram(
|
|
69
|
+
program: TestProgram,
|
|
70
|
+
config: BlueprintRunnerConfig,
|
|
71
|
+
): Promise<TestProgramResult> {
|
|
72
|
+
const start = performance.now();
|
|
73
|
+
const scenarioResults: ScenarioResult[] = [];
|
|
74
|
+
let allPassed = true;
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < program.scenarios.length; i++) {
|
|
77
|
+
if (config.abortSignal?.aborted) break;
|
|
78
|
+
|
|
79
|
+
const result = await runBlueprintScenario(
|
|
80
|
+
program.scenarios[i],
|
|
81
|
+
program.definitions,
|
|
82
|
+
i,
|
|
83
|
+
config,
|
|
84
|
+
);
|
|
85
|
+
scenarioResults.push(result);
|
|
86
|
+
if (!result.passed) allPassed = false;
|
|
87
|
+
config.onScenarioComplete?.(result);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
passed: allPassed,
|
|
92
|
+
scenarioResults,
|
|
93
|
+
durationMs: performance.now() - start,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Execute a single scenario as a Blueprint on a Player.
|
|
99
|
+
*/
|
|
100
|
+
export async function runBlueprintScenario(
|
|
101
|
+
scenario: TestScenario,
|
|
102
|
+
definitions: Record<string, PlayerWorkflowDefinition>,
|
|
103
|
+
scenarioIndex: number,
|
|
104
|
+
config: BlueprintRunnerConfig,
|
|
105
|
+
): Promise<ScenarioResult> {
|
|
106
|
+
const start = performance.now();
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// 1. Compile the scenario to a Blueprint
|
|
110
|
+
const blueprint = compileTestScenario(scenario, definitions, scenarioIndex);
|
|
111
|
+
|
|
112
|
+
// 2. Create action handlers based on execution mode
|
|
113
|
+
const { handlers, state: testState } = createActionHandlers(definitions, config);
|
|
114
|
+
|
|
115
|
+
// 3. Create the Player (StateMachine) with the compiled Blueprint
|
|
116
|
+
const evaluator = createEvaluator({
|
|
117
|
+
functions: [],
|
|
118
|
+
failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const sm = new StateMachine(blueprint, {}, { evaluator, actionHandlers: handlers });
|
|
122
|
+
|
|
123
|
+
// Bind the orchestrator reference so handlers can set fields
|
|
124
|
+
testState.setOrchestratorField = (field: string, value: unknown) => {
|
|
125
|
+
sm.setField(field, value);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// 4. Drive the Player through the test steps
|
|
129
|
+
// SM starts in __idle. Transition 'setup' → __setup (creates instances).
|
|
130
|
+
// Then 'next' for each step.
|
|
131
|
+
const stepCount = scenario.steps.length;
|
|
132
|
+
|
|
133
|
+
// Setup: __idle → __setup (fires on_enter which creates test instances)
|
|
134
|
+
const setupResult = await sm.transition('setup');
|
|
135
|
+
if (!setupResult.success) {
|
|
136
|
+
return {
|
|
137
|
+
scenarioName: scenario.name,
|
|
138
|
+
passed: false,
|
|
139
|
+
stepResults: [],
|
|
140
|
+
durationMs: performance.now() - start,
|
|
141
|
+
error: `Setup failed: ${setupResult.error}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < stepCount; i++) {
|
|
146
|
+
if (config.abortSignal?.aborted) break;
|
|
147
|
+
|
|
148
|
+
const result = await sm.transition('next');
|
|
149
|
+
if (!result.success) {
|
|
150
|
+
// Transition itself failed — record as step error
|
|
151
|
+
testState.stepResults.push({
|
|
152
|
+
stepName: scenario.steps[i]?.name ?? `step_${i}`,
|
|
153
|
+
passed: false,
|
|
154
|
+
assertionResults: [],
|
|
155
|
+
durationMs: 0,
|
|
156
|
+
error: `Blueprint transition failed: ${result.error}`,
|
|
157
|
+
});
|
|
158
|
+
testState.failed = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Report step completion
|
|
163
|
+
const lastStep = testState.stepResults[testState.stepResults.length - 1];
|
|
164
|
+
if (lastStep) {
|
|
165
|
+
config.onStepComplete?.(scenarioIndex, lastStep);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if step failed
|
|
169
|
+
if (sm.stateData.__test_step_failed) {
|
|
170
|
+
await sm.transition('abort');
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 5. Advance to terminal state if all steps passed
|
|
176
|
+
if (!testState.failed && !config.abortSignal?.aborted) {
|
|
177
|
+
await sm.transition('next'); // → __pass
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const passed = !testState.failed;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
scenarioName: scenario.name,
|
|
184
|
+
passed,
|
|
185
|
+
stepResults: testState.stepResults,
|
|
186
|
+
durationMs: performance.now() - start,
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return {
|
|
190
|
+
scenarioName: scenario.name,
|
|
191
|
+
passed: false,
|
|
192
|
+
stepResults: [],
|
|
193
|
+
durationMs: performance.now() - start,
|
|
194
|
+
error: error instanceof Error ? error.message : String(error),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// =============================================================================
|
|
200
|
+
// Internals
|
|
201
|
+
// =============================================================================
|
|
202
|
+
|
|
203
|
+
function createActionHandlers(
|
|
204
|
+
definitions: Record<string, PlayerWorkflowDefinition>,
|
|
205
|
+
config: BlueprintRunnerConfig,
|
|
206
|
+
): { handlers: Map<string, ActionHandler>; state: TestActionState } {
|
|
207
|
+
if (config.mode === 'api') {
|
|
208
|
+
return createApiTestActions(config.adapter, {
|
|
209
|
+
settleDelayMs: config.settleDelayMs,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return createInProcessTestActions(definitions);
|
|
214
|
+
}
|