@systemfsoftware/xstate-test 0.0.0-bootstrap.0 → 2.0.0-beta.2

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.
@@ -0,0 +1,4660 @@
1
+ import { t as ensuringFinalizerWins } from "./finalizerError-DOptAIDs.mjs";
2
+ import { eventsFromSchemas, isTypeOnlySchema } from "./schema.mjs";
3
+ import { dual } from "effect/Function";
4
+ import { SimulatedClock, createActor, createAsyncLogic, getInitialMicrosteps, getMicrosteps, initialTransition, transition } from "@systemfsoftware/xstate";
5
+ import { getDescendantStateNodes, getPathsFromEvents, getShortestPaths, getSimplePaths, serializeSnapshot } from "@systemfsoftware/xstate/graph";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Ref from "effect/Ref";
8
+ import * as Cause from "effect/Cause";
9
+ import { squash } from "effect/Cause";
10
+ import * as Data from "effect/Data";
11
+ import * as DateTime from "effect/DateTime";
12
+ import * as Exit from "effect/Exit";
13
+ import * as Logger from "effect/Logger";
14
+ import * as fc from "fast-check";
15
+ // src/engine/coverage.ts
16
+ function dimension() {
17
+ return {
18
+ counts: {},
19
+ declarations: /* @__PURE__ */ new Map()
20
+ };
21
+ }
22
+ function declare(target, id, declaration = {}) {
23
+ const previous = target.declarations.get(id);
24
+ target.declarations.set(id, {
25
+ unreachable: (previous?.unreachable ?? false) || (declaration.unreachable ?? false),
26
+ unknown: (previous?.unknown ?? false) || (declaration.unknown ?? false)
27
+ });
28
+ }
29
+ function declareAggregate(target, id, declaration) {
30
+ const previous = target.declarations.get(id);
31
+ target.declarations.set(id, {
32
+ unreachable: previous !== void 0 ? previous.unreachable && declaration.unreachable : declaration.unreachable,
33
+ unknown: previous !== void 0 ? previous.unknown && declaration.unknown : declaration.unknown
34
+ });
35
+ }
36
+ const incrementCoverage = dual(2, function incrementCoverage(target, id) {
37
+ target.counts[id] = (target.counts[id] ?? 0) + 1;
38
+ });
39
+ function getPropertyConfigurationId(snapshot) {
40
+ const nodes = snapshot.nodes ?? snapshot._nodes;
41
+ return JSON.stringify((nodes?.map((node) => node.id) ?? []).sort());
42
+ }
43
+ function stableSerialize(value) {
44
+ if (Array.isArray(value)) return `[${value.map(stableSerialize).join(",")}]`;
45
+ if (value !== void 0 && value !== null && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`).join(",")}}`;
46
+ return JSON.stringify(value) ?? String(value);
47
+ }
48
+ function getPropertyStateId(snapshot) {
49
+ return stableSerialize(snapshot.value ?? null);
50
+ }
51
+ function getPropertyTransitionId(coverage, transition) {
52
+ return coverage.transitionIds.get(transition) ?? JSON.stringify([
53
+ "transition",
54
+ transition.source.id,
55
+ transition.eventType || "@eventless",
56
+ "dynamic"
57
+ ]);
58
+ }
59
+ /**
60
+ * Resolves the default target(s) of a history state node. History nodes are
61
+ * never part of an active configuration themselves; entering one enters these
62
+ * nodes instead, so anything reachable only through a history default target
63
+ * would otherwise be misreported as unreachable.
64
+ */
65
+ function getHistoryDefaultTargets(node) {
66
+ const parent = node.parent;
67
+ if (parent === void 0) return [];
68
+ const configTarget = node.config.target;
69
+ const normalized = configTarget === void 0 || configTarget === "" ? void 0 : Array.isArray(configTarget) ? configTarget : [configTarget];
70
+ if (normalized === void 0) return parent.type === "parallel" ? [parent] : parent.initial?.target ?? [];
71
+ const targets = [];
72
+ for (const target of normalized) {
73
+ if (typeof target !== "string") {
74
+ targets.push(target);
75
+ continue;
76
+ }
77
+ try {
78
+ targets.push(resolveStateNodePath(parent, target));
79
+ } catch {}
80
+ }
81
+ return targets;
82
+ }
83
+ /** Resolves a `'#id'` or (escaped) dot-separated path relative to `node`. */
84
+ function resolveStateNodePath(node, path) {
85
+ if (path.startsWith("#")) try {
86
+ return node.machine.getStateNodeById(path);
87
+ } catch {}
88
+ const segments = [];
89
+ let segment = "";
90
+ for (let index = 0; index < path.length; index++) {
91
+ const char = path[index];
92
+ if (char === "\\") segment += path[++index] ?? "";
93
+ else if (char === ".") {
94
+ segments.push(segment);
95
+ segment = "";
96
+ } else segment += char;
97
+ }
98
+ segments.push(segment);
99
+ let current = node;
100
+ for (const key of segments) {
101
+ if (key.length === 0) break;
102
+ const child = current.states[key];
103
+ if (child === void 0) throw new Error(`Child state '${key}' does not exist on '${current.id}'`);
104
+ current = child;
105
+ }
106
+ return current;
107
+ }
108
+ function collectReachableNodes(root) {
109
+ const reachable = /* @__PURE__ */ new Set([root.id]);
110
+ const queue = [root];
111
+ while (queue.length !== 0) {
112
+ const node = queue.shift();
113
+ const candidates = [
114
+ ...node.initial?.target ?? [],
115
+ ...node.type === "parallel" ? Object.values(node.states) : [],
116
+ ...node.type === "history" ? getHistoryDefaultTargets(node) : [],
117
+ ...[...node.transitions.values()].flatMap((definitions) => definitions.flatMap((definition) => definition.target ?? [])),
118
+ ...(node.always ?? []).flatMap((definition) => definition.target ?? [])
119
+ ];
120
+ for (const target of candidates) if (!reachable.has(target.id)) {
121
+ reachable.add(target.id);
122
+ queue.push(target);
123
+ }
124
+ }
125
+ return reachable;
126
+ }
127
+ function registerTransition(coverage, transition, index, reachable, reachabilityUnknown) {
128
+ const id = JSON.stringify([
129
+ "transition",
130
+ transition.source.id,
131
+ transition.eventType || "@eventless",
132
+ index
133
+ ]);
134
+ const sourceUnreachable = !reachable.has(transition.source.id);
135
+ coverage.transitionIds.set(transition, id);
136
+ declare(coverage.transitions, id, {
137
+ unreachable: sourceUnreachable && !reachabilityUnknown,
138
+ unknown: sourceUnreachable && reachabilityUnknown
139
+ });
140
+ declareAggregate(coverage.eventTypes, transition.eventType || "@eventless", {
141
+ unreachable: sourceUnreachable && !reachabilityUnknown,
142
+ unknown: sourceUnreachable && reachabilityUnknown
143
+ });
144
+ return id;
145
+ }
146
+ /**
147
+ * Upper bound on statically enumerated transition pairs. Pair universes grow
148
+ * quadratically, so large machines report a truncated universe rather than
149
+ * spending unbounded time and memory on it.
150
+ */
151
+ const TRANSITION_PAIR_UNIVERSE_LIMIT = 2e3;
152
+ /**
153
+ * Machines with more transitions than this skip pair enumeration entirely,
154
+ * since even the static pair universe can grow as O(T^2).
155
+ */
156
+ const TRANSITION_PAIR_MACHINE_LIMIT = 500;
157
+ function getPropertyTransitionPairId(first, second) {
158
+ return `${first} -> ${second}`;
159
+ }
160
+ function getDescendantIds(node) {
161
+ const ids = /* @__PURE__ */ new Set([node.id]);
162
+ for (const descendant of getDescendantStateNodes(node)) ids.add(descendant.id);
163
+ return ids;
164
+ }
165
+ /**
166
+ * Declares the pairs of transitions that can run back to back. A pair
167
+ * `(t1, t2)` is possible when `t2`'s source node is within the configuration
168
+ * `t1` can leave behind, approximated as the descendants-or-self of `t1`'s
169
+ * targets (or of its own source when `t1` is targetless).
170
+ *
171
+ * Only pairs of static transitions are declared. A dynamic transition's target
172
+ * is unknown until it runs, so pairs involving one are not part of the
173
+ * universe; when observed at runtime they are still reported as covered.
174
+ */
175
+ function declareTransitionPairs(coverage, registered) {
176
+ if (registered.length > TRANSITION_PAIR_MACHINE_LIMIT) {
177
+ coverage.transitionPairsTruncated = true;
178
+ return;
179
+ }
180
+ const staticTransitions = registered.filter((entry) => entry.transition.to === void 0);
181
+ const bySource = /* @__PURE__ */ new Map();
182
+ for (const entry of staticTransitions) {
183
+ const sourceId = entry.transition.source.id;
184
+ let entries = bySource.get(sourceId);
185
+ if (entries === void 0) {
186
+ entries = [];
187
+ bySource.set(sourceId, entries);
188
+ }
189
+ entries.push(entry);
190
+ }
191
+ const descendants = /* @__PURE__ */ new Map();
192
+ let declared = 0;
193
+ for (const first of staticTransitions) {
194
+ const roots = first.transition.target !== void 0 && first.transition.target.length !== 0 ? first.transition.target : [first.transition.source];
195
+ const firstDeclaration = coverage.transitions.declarations.get(first.id);
196
+ const reachableSources = /* @__PURE__ */ new Set();
197
+ for (const root of roots) {
198
+ let ids = descendants.get(root.id);
199
+ if (ids === void 0) {
200
+ ids = getDescendantIds(root);
201
+ descendants.set(root.id, ids);
202
+ }
203
+ for (const id of ids) reachableSources.add(id);
204
+ }
205
+ for (const sourceId of reachableSources) for (const second of bySource.get(sourceId) ?? []) {
206
+ if (declared >= TRANSITION_PAIR_UNIVERSE_LIMIT) {
207
+ coverage.transitionPairsTruncated = true;
208
+ return;
209
+ }
210
+ declared++;
211
+ const secondDeclaration = coverage.transitions.declarations.get(second.id);
212
+ declare(coverage.transitionPairs, getPropertyTransitionPairId(first.id, second.id), {
213
+ unreachable: firstDeclaration?.unreachable === true || secondDeclaration?.unreachable === true,
214
+ unknown: firstDeclaration?.unknown === true || secondDeclaration?.unknown === true
215
+ });
216
+ }
217
+ }
218
+ }
219
+ function normalizeRequirements(meta) {
220
+ const requirements = meta?.requirements;
221
+ if (typeof requirements === "string") return [requirements];
222
+ if (Array.isArray(requirements)) return requirements.filter((requirement) => typeof requirement === "string");
223
+ return [];
224
+ }
225
+ function declareRequirements(coverage, requirements, source, owner, ownerId) {
226
+ if (requirements.length === 0) return;
227
+ owner.set(ownerId, [...owner.get(ownerId) ?? [], ...requirements]);
228
+ for (const requirement of requirements) {
229
+ declare(coverage.requirements, requirement);
230
+ const sources = coverage.requirementSources[requirement] ??= [];
231
+ if (!sources.includes(source)) sources.push(source);
232
+ }
233
+ }
234
+ function recordRequirements(coverage, requirements) {
235
+ for (const requirement of requirements ?? []) incrementCoverage(coverage.requirements, requirement);
236
+ }
237
+ function createTestCoverage(logic) {
238
+ const coverage = {
239
+ runs: 0,
240
+ steps: 0,
241
+ skipped: 0,
242
+ prefixSteps: 0,
243
+ generatedSteps: 0,
244
+ invariantChecks: 0,
245
+ temporalChecks: 0,
246
+ clockAdvances: 0,
247
+ checkpoints: 0,
248
+ stops: 0,
249
+ sutComparisons: 0,
250
+ oracleComparisons: 0,
251
+ states: dimension(),
252
+ stateNodes: dimension(),
253
+ configurations: dimension(),
254
+ statuses: dimension(),
255
+ eventTypes: dimension(),
256
+ eventCases: {},
257
+ transitions: dimension(),
258
+ transitionPairs: dimension(),
259
+ transitionPairsTruncated: false,
260
+ requirements: dimension(),
261
+ requirementSources: {},
262
+ requirementsByStateNode: /* @__PURE__ */ new Map(),
263
+ requirementsByTransition: /* @__PURE__ */ new Map(),
264
+ previousTransitionIds: null,
265
+ frontiers: dimension(),
266
+ labels: {},
267
+ temporal: {
268
+ counts: {},
269
+ campaignFailed: /* @__PURE__ */ new Set(),
270
+ warnings: []
271
+ },
272
+ shrinkRuns: 0,
273
+ transitionIds: /* @__PURE__ */ new WeakMap(),
274
+ maximumObservedSequenceLength: 0,
275
+ pendingActorSteps: 0
276
+ };
277
+ const machine = logic;
278
+ if (machine.root === void 0) {
279
+ for (const target of [
280
+ coverage.states,
281
+ coverage.stateNodes,
282
+ coverage.configurations,
283
+ coverage.eventTypes,
284
+ coverage.transitions,
285
+ coverage.transitionPairs
286
+ ]) declare(target, "(not statically enumerable)", { unknown: true });
287
+ return coverage;
288
+ }
289
+ const nodes = [machine.root, ...getDescendantStateNodes(machine.root)];
290
+ const reachable = collectReachableNodes(machine.root);
291
+ const hasReachableDynamicTransition = nodes.some((node) => reachable.has(node.id) && ([...node.transitions.values()].some((definitions) => definitions.some((definition) => definition.to !== void 0)) || (node.always ?? []).some((definition) => definition.to !== void 0)));
292
+ const registered = [];
293
+ for (const node of nodes) {
294
+ declareRequirements(coverage, normalizeRequirements(node.meta), `stateNode:${node.id}`, coverage.requirementsByStateNode, node.id);
295
+ declare(coverage.stateNodes, node.id, {
296
+ unreachable: !reachable.has(node.id) && !hasReachableDynamicTransition,
297
+ unknown: !reachable.has(node.id) && hasReachableDynamicTransition
298
+ });
299
+ for (const definitions of node.transitions.values()) for (let index = 0; index < definitions.length; index++) {
300
+ const definition = definitions[index];
301
+ if (definition === void 0) continue;
302
+ registered.push({
303
+ id: registerTransition(coverage, definition, index, reachable, !reachable.has(node.id) && hasReachableDynamicTransition),
304
+ transition: definition
305
+ });
306
+ }
307
+ const always = node.always ?? [];
308
+ for (let index = 0; index < always.length; index++) {
309
+ const definition = always[index];
310
+ if (definition === void 0) continue;
311
+ registered.push({
312
+ id: registerTransition(coverage, definition, index, reachable, !reachable.has(node.id) && hasReachableDynamicTransition),
313
+ transition: definition
314
+ });
315
+ }
316
+ }
317
+ for (const entry of registered) declareRequirements(coverage, normalizeRequirements(entry.transition.meta), `transition:${entry.id}`, coverage.requirementsByTransition, entry.id);
318
+ declareTransitionPairs(coverage, registered);
319
+ declare(coverage.states, "(runtime serialized states)", { unknown: true });
320
+ declare(coverage.configurations, "(runtime configurations)", { unknown: true });
321
+ return coverage;
322
+ }
323
+ const recordPropertySnapshot = dual(2, function(coverage, snapshot) {
324
+ incrementCoverage(coverage.states, getPropertyStateId(snapshot));
325
+ incrementCoverage(coverage.configurations, getPropertyConfigurationId(snapshot));
326
+ incrementCoverage(coverage.statuses, snapshot.status);
327
+ const nodes = snapshot.nodes ?? snapshot._nodes;
328
+ for (const node of nodes ?? []) {
329
+ incrementCoverage(coverage.stateNodes, node.id);
330
+ recordRequirements(coverage, coverage.requirementsByStateNode.get(node.id));
331
+ }
332
+ });
333
+ const recordPropertyTransitions = dual(3, function(coverage, event, transitions) {
334
+ incrementCoverage(coverage.eventTypes, event.type);
335
+ const ids = [];
336
+ for (const selected of transitions) {
337
+ const id = getPropertyTransitionId(coverage, selected);
338
+ ids.push(id);
339
+ incrementCoverage(coverage.transitions, id);
340
+ recordRequirements(coverage, coverage.requirementsByTransition.get(id));
341
+ }
342
+ if (ids.length !== 0) {
343
+ for (const previous of coverage.previousTransitionIds ?? []) for (const current of ids) incrementCoverage(coverage.transitionPairs, getPropertyTransitionPairId(previous, current));
344
+ coverage.previousTransitionIds = ids;
345
+ }
346
+ return ids;
347
+ });
348
+ /**
349
+ * Clears the consecutive-transition chain so pairs are only counted within a
350
+ * single run.
351
+ */
352
+ function resetPropertyTransitionPairs(coverage) {
353
+ coverage.previousTransitionIds = null;
354
+ }
355
+ const getPropertyEventCaseId = dual(2, function(eventType, caseName) {
356
+ return JSON.stringify([
357
+ "event-case",
358
+ eventType,
359
+ caseName
360
+ ]);
361
+ });
362
+ /**
363
+ * Parses an id produced by {@link getPropertyEventCaseId} back into its event
364
+ * type and case name. Returns `undefined` for ids of any other shape.
365
+ */
366
+ function parsePropertyEventCaseId(id) {
367
+ let parsed;
368
+ try {
369
+ parsed = JSON.parse(id);
370
+ } catch {
371
+ return;
372
+ }
373
+ if (!Array.isArray(parsed) || parsed.length !== 3 || parsed[0] !== "event-case" || typeof parsed[1] !== "string" || typeof parsed[2] !== "string") return;
374
+ return {
375
+ type: parsed[1],
376
+ name: parsed[2]
377
+ };
378
+ }
379
+ const declarePropertyEventCase = dual(3, function(coverage, id, weight) {
380
+ const existing = coverage.eventCases[id];
381
+ if (existing === void 0) {
382
+ coverage.eventCases[id] = {
383
+ weight: weight ?? 1,
384
+ generated: 0,
385
+ applicable: 0,
386
+ executed: 0,
387
+ ignored: 0
388
+ };
389
+ return;
390
+ }
391
+ if (weight !== void 0 && existing.weight !== weight) coverage.eventCases[id] = {
392
+ ...existing,
393
+ weight
394
+ };
395
+ });
396
+ const recordPropertyEventCase = dual(3, function(coverage, id, stage) {
397
+ declarePropertyEventCase(coverage, id, void 0);
398
+ const counts = coverage.eventCases[id];
399
+ counts[stage]++;
400
+ });
401
+ function finalizeDimension(dimension) {
402
+ const covered = Object.keys(dimension.counts).sort();
403
+ const uncovered = [];
404
+ const unreachable = [];
405
+ const unknown = [];
406
+ for (const [id, declaration] of dimension.declarations) {
407
+ if (dimension.counts[id] !== void 0) continue;
408
+ if (declaration.unknown) unknown.push(id);
409
+ else if (declaration.unreachable) unreachable.push(id);
410
+ else uncovered.push(id);
411
+ }
412
+ return {
413
+ counts: { ...dimension.counts },
414
+ covered,
415
+ uncovered: uncovered.sort(),
416
+ unreachable: unreachable.sort(),
417
+ unknown: unknown.sort()
418
+ };
419
+ }
420
+ const finalizeTestCoverage = dual((args) => args.length >= 2 || typeof args[0] === "object" && args[0] !== null && "eventCases" in args[0], function(coverage, exploration = {
421
+ strategy: "property",
422
+ mode: "pure",
423
+ configuredRuns: null,
424
+ completedRuns: coverage.runs,
425
+ attemptedRuns: coverage.runs,
426
+ shrinkRuns: coverage.shrinkRuns,
427
+ maximumSequenceLength: null,
428
+ maximumObservedSequenceLength: coverage.maximumObservedSequenceLength,
429
+ frontiers: [],
430
+ seeds: [],
431
+ swarm: null,
432
+ target: {
433
+ best: -Infinity,
434
+ improvements: 0
435
+ },
436
+ truncated: false,
437
+ truncationReasons: [],
438
+ stoppedBecause: "budget",
439
+ pendingActorSteps: coverage.pendingActorSteps
440
+ }) {
441
+ const labelRuns = (exploration.attemptedRuns || coverage.runs) - coverage.shrinkRuns;
442
+ const temporalCounts = coverage.temporal.counts;
443
+ const temporalIds = Object.keys(temporalCounts).sort();
444
+ const temporalCountsOf = (id) => {
445
+ const counts = temporalCounts[id];
446
+ if (counts === void 0) throw new TypeError(`Missing temporal counts for '${id}'`);
447
+ return counts;
448
+ };
449
+ return {
450
+ runs: coverage.runs,
451
+ steps: coverage.steps,
452
+ skipped: coverage.skipped,
453
+ prefixSteps: coverage.prefixSteps,
454
+ generatedSteps: coverage.generatedSteps,
455
+ invariantChecks: coverage.invariantChecks,
456
+ temporalChecks: coverage.temporalChecks,
457
+ clockAdvances: coverage.clockAdvances,
458
+ checkpoints: coverage.checkpoints,
459
+ stops: coverage.stops,
460
+ sutComparisons: coverage.sutComparisons,
461
+ oracleComparisons: coverage.oracleComparisons,
462
+ states: finalizeDimension(coverage.states),
463
+ stateNodes: finalizeDimension(coverage.stateNodes),
464
+ configurations: finalizeDimension(coverage.configurations),
465
+ statuses: finalizeDimension(coverage.statuses),
466
+ eventTypes: finalizeDimension(coverage.eventTypes),
467
+ eventCases: Object.fromEntries(Object.entries(coverage.eventCases).sort(([left], [right]) => left.localeCompare(right)).map(([id, counts]) => [id, { ...counts }])),
468
+ transitions: finalizeDimension(coverage.transitions),
469
+ transitionPairs: {
470
+ ...finalizeDimension(coverage.transitionPairs),
471
+ truncated: coverage.transitionPairsTruncated
472
+ },
473
+ requirements: {
474
+ ...finalizeDimension(coverage.requirements),
475
+ sources: Object.fromEntries(Object.entries(coverage.requirementSources).sort(([left], [right]) => left.localeCompare(right)).map(([id, sources]) => [id, [...sources].sort()]))
476
+ },
477
+ frontiers: finalizeDimension(coverage.frontiers),
478
+ labels: Object.fromEntries(Object.entries(coverage.labels).sort(([left], [right]) => left.localeCompare(right)).map(([name, entry]) => [name, {
479
+ count: entry.count,
480
+ values: Object.fromEntries(Object.entries(entry.values).sort(([left], [right]) => left.localeCompare(right))),
481
+ share: labelRuns !== 0 ? Math.min(1, entry.runs / labelRuns) : 0
482
+ }])),
483
+ temporal: {
484
+ satisfied: temporalIds.filter((id) => temporalCountsOf(id).satisfied > 0),
485
+ failed: [.../* @__PURE__ */ new Set([...temporalIds.filter((id) => temporalCountsOf(id).failed > 0), ...coverage.temporal.campaignFailed])].sort(),
486
+ inconclusive: temporalIds.filter((id) => temporalCountsOf(id).inconclusive > 0 && temporalCountsOf(id).satisfied === 0 && !coverage.temporal.campaignFailed.has(id)),
487
+ counts: Object.fromEntries(temporalIds.map((id) => [id, { ...temporalCountsOf(id) }])),
488
+ warnings: coverage.temporal.warnings.slice()
489
+ },
490
+ exploration
491
+ };
492
+ });
493
+ /**
494
+ * Records one occurrence of a label. `seen` is the set of label names already
495
+ * recorded in the current run, so each run contributes at most once to a
496
+ * label's `share`.
497
+ */
498
+ const recordPropertyLabel = dual(4, function(coverage, name, value, seen) {
499
+ const entry = coverage.labels[name] ??= {
500
+ count: 0,
501
+ values: {},
502
+ runs: 0
503
+ };
504
+ entry.count++;
505
+ if (value !== void 0) {
506
+ const key = String(value);
507
+ entry.values[key] = (entry.values[key] ?? 0) + 1;
508
+ }
509
+ if (!seen.has(name)) {
510
+ seen.add(name);
511
+ entry.runs++;
512
+ }
513
+ });
514
+ /** Records one run's outcome for a temporal property. */
515
+ const recordPropertyTemporal = dual(3, function(coverage, id, outcome) {
516
+ const counts = coverage.temporal.counts[id] ??= {
517
+ satisfied: 0,
518
+ failed: 0,
519
+ inconclusive: 0
520
+ };
521
+ counts[outcome]++;
522
+ });
523
+ const declarePropertyFrontier = dual(2, function(coverage, id) {
524
+ declare(coverage.frontiers, id);
525
+ });
526
+ function isStateMachine(logic) {
527
+ const machine = logic;
528
+ return machine.root !== void 0 && typeof machine.getStateNodeById === "function" && typeof machine.getTransitionData === "function";
529
+ }
530
+ function withMicrostepDetails(result, getSteps) {
531
+ let microsteps;
532
+ try {
533
+ microsteps = getSteps();
534
+ } catch {
535
+ microsteps = [];
536
+ }
537
+ return [
538
+ result[0],
539
+ result[1],
540
+ microsteps.flatMap(([, , transitions]) => transitions)
541
+ ];
542
+ }
543
+ /**
544
+ * The pure `transition()`, also returning the transitions taken (from
545
+ * `getMicrosteps()`) for coverage attribution.
546
+ */
547
+ const transitionWithDetails = dual(3, function(logic, snapshot, event) {
548
+ const result = transition(logic, snapshot, event);
549
+ if (!isStateMachine(logic) || result[0] === snapshot) return [
550
+ result[0],
551
+ result[1],
552
+ []
553
+ ];
554
+ return withMicrostepDetails(result, () => getMicrosteps(logic, snapshot, event));
555
+ });
556
+ /** The pure `initialTransition()`, with the details of {@link transitionWithDetails}. */
557
+ const initialTransitionWithDetails = dual(2, function(logic, input) {
558
+ const result = initialTransition(logic, input);
559
+ if (!isStateMachine(logic)) return [
560
+ result[0],
561
+ result[1],
562
+ []
563
+ ];
564
+ return withMicrostepDetails(result, () => getInitialMicrosteps(logic, input));
565
+ });
566
+ // src/engine/utils.ts
567
+ function simpleStringify(value) {
568
+ return JSON.stringify(value);
569
+ }
570
+ /** FNV-1a over a string, as an unsigned 32-bit integer. */
571
+ function fnv1a(value) {
572
+ let hash = 2166136261;
573
+ for (let index = 0; index < value.length; index++) {
574
+ hash ^= value.charCodeAt(index);
575
+ hash = Math.imul(hash, 16777619);
576
+ }
577
+ return hash >>> 0;
578
+ }
579
+ /** A deterministic 32-bit PRNG (mulberry32), seeded by `seed`. */
580
+ function createSeededRng(seed) {
581
+ let a = seed >>> 0;
582
+ return () => {
583
+ a = a + 1831565813 >>> 0;
584
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
585
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
586
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
587
+ };
588
+ }
589
+ /** Events synthesized from the transitions of the active state nodes. */
590
+ function getAllOwnEvents(snapshot) {
591
+ const events = snapshot.nodes.flatMap((stateNode) => [...stateNode.transitions.values()].flatMap((transitions) => transitions.map((transition) => {
592
+ const event = {
593
+ type: transition.eventType,
594
+ ...transition.matches
595
+ };
596
+ if ("actorId" in event && (event.type === "xstate.done.actor" || event.type === "xstate.error.actor" || event.type === "xstate.snapshot.actor" || event.type === "xstate.timeout.actor")) event["sessionId"] = snapshot.children[event["actorId"]]?.sessionId;
597
+ return event;
598
+ })));
599
+ return events.filter((event, index) => events.findIndex((candidate) => {
600
+ const keys = Object.keys(event);
601
+ return keys.length === Object.keys(candidate).length && keys.every((key) => Object.is(event[key], candidate[key]));
602
+ }) === index);
603
+ }
604
+ // src/engine/eventDescriptors.ts
605
+ /**
606
+ * Distinguishes the descriptor form (`{ generate, case?, weight?, ... }`) from
607
+ * a bare generator. Generators are opaque adapter values that can themselves
608
+ * be objects with a `generate` *method* (fast-check arbitraries have one), so
609
+ * a `generate` key only marks a descriptor when it does not hold a function;
610
+ * any of the descriptor-only keys marks one regardless.
611
+ * @experimental
612
+ */
613
+ function isEventDescriptorObject(value) {
614
+ if (value === null || typeof value !== "object") return false;
615
+ if ("case" in value || "when" in value || "resolve" in value || "weight" in value) return true;
616
+ return Object.prototype.hasOwnProperty.call(value, "generate") && typeof value.generate !== "function";
617
+ }
618
+ /** @experimental */
619
+ const assertTestWeight = dual(2, function assertTestWeight(weight, location) {
620
+ if (weight === void 0) return 1;
621
+ if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) throw new Error(`Property ${location} has an invalid \`weight\` (${String(weight)}). Weights must be positive, finite numbers.`);
622
+ return weight;
623
+ });
624
+ /**
625
+ * Normalizes the shared `events` option — a map of event type to a bare
626
+ * generator, a descriptor object, or an array of either — into the flat list of
627
+ * declared cases both `propertyTest()` and `testPaths()` work from.
628
+ * @experimental
629
+ */
630
+ function normalizeEventDescriptors(events) {
631
+ const descriptors = /* @__PURE__ */ new Map();
632
+ const cases = [];
633
+ for (const [type, configured] of Object.entries(events ?? {})) {
634
+ const configuredCases = Array.isArray(configured) ? configured : [configured];
635
+ for (const eventCase of configuredCases) {
636
+ const descriptor = isEventDescriptorObject(eventCase) ? eventCase : { generate: eventCase };
637
+ const caseName = descriptor.case ?? "default";
638
+ if (caseName.length === 0) throw new Error(`Property event case for "${type}" must not be empty`);
639
+ const caseId = getPropertyEventCaseId(type, caseName);
640
+ if (descriptors.has(caseId)) throw new Error(`Property event case "${caseName}" is duplicated for "${type}"`);
641
+ descriptors.set(caseId, descriptor);
642
+ cases.push({
643
+ type,
644
+ caseName,
645
+ caseId,
646
+ generator: descriptor.generate,
647
+ weight: assertTestWeight(descriptor.weight, `event case "${caseName}" for "${type}"`),
648
+ descriptor
649
+ });
650
+ }
651
+ }
652
+ return {
653
+ cases,
654
+ descriptors
655
+ };
656
+ }
657
+ /**
658
+ * The per-case seed used when sampling payloads. Deriving it from the case id
659
+ * keeps each case's samples stable when other cases are added or removed.
660
+ * @experimental
661
+ */
662
+ const deriveCaseSeed = dual(2, function deriveCaseSeed(seed, caseId) {
663
+ return (seed ^ fnv1a(caseId)) >>> 0;
664
+ });
665
+ /**
666
+ * Draws `count` values from a {@link TestGenerator}.
667
+ *
668
+ * @experimental
669
+ */
670
+ const sampleGenerator = dual(3, function sampleGenerator(generator, rng, count) {
671
+ const draw = typeof generator === "function" ? generator : typeof generator?.sample === "function" ? (rng2) => generator.sample(rng2) : void 0;
672
+ if (draw === void 0) throw new Error("Path generation requires each `generate` value to be a function `(rng) => value` or an object with a `sample(rng)` method. In `@xstate/test`, fast-check arbitraries are adapted automatically.");
673
+ const values = [];
674
+ for (let index = 0; index < count; index++) values.push(draw(rng));
675
+ return values;
676
+ });
677
+ /**
678
+ * Builds a {@link TestPickDescriptor} around any index generator. `pick()` in
679
+ * `xstate/graph` and in `@xstate/test` differ only in the generator.
680
+ * @experimental
681
+ */
682
+ const createPickDescriptor = dual(3, function createPickDescriptor(generate, select, toPayload) {
683
+ return {
684
+ generate,
685
+ resolve: ({ snapshot, generated }) => {
686
+ const items = select(snapshot);
687
+ if (items.length === 0) return;
688
+ const item = items[Math.abs(Math.trunc(generated)) % items.length];
689
+ if (item === void 0) return;
690
+ return toPayload !== void 0 ? toPayload(item, snapshot) : item;
691
+ }
692
+ };
693
+ });
694
+ // src/engine/constants.ts
695
+ const XSTATE_INIT = "@xstate.init";
696
+ const XSTATE_STOP = "@xstate.stop";
697
+ // src/engine/outcomes.ts
698
+ /**
699
+ * Queues and hands out actor outcomes for stubbed invoke sources.
700
+ *
701
+ * A stub actor asks the registry for its outcome when it starts. If an
702
+ * outcome is already queued for its source it resolves immediately; otherwise
703
+ * the stub stays pending until an `outcome` command (or a seeded replay
704
+ * record) supplies one, which is what lets fast-check shrink service results.
705
+ */
706
+ var PropertyOutcomeRegistry = class {
707
+ queued = /* @__PURE__ */ new Map();
708
+ waiting = /* @__PURE__ */ new Map();
709
+ /** Called by a stub actor when it starts. */
710
+ request(src) {
711
+ const next = this.queued.get(src)?.shift();
712
+ if (next !== void 0) return Promise.resolve(next);
713
+ return Effect.runPromise(Effect.callback((resume) => {
714
+ const resolve = (outcome) => {
715
+ resume(Effect.succeed(outcome));
716
+ };
717
+ const waiters = this.waiting.get(src);
718
+ if (waiters !== void 0) waiters.push(resolve);
719
+ else this.waiting.set(src, [resolve]);
720
+ }));
721
+ }
722
+ /** Resolves the oldest pending stub for `src`, or queues for the next one. */
723
+ provide(src, outcome) {
724
+ const waiter = this.waiting.get(src)?.shift();
725
+ if (waiter !== void 0) {
726
+ waiter(outcome);
727
+ return;
728
+ }
729
+ const queue = this.queued.get(src);
730
+ if (queue !== void 0) queue.push(outcome);
731
+ else this.queued.set(src, [outcome]);
732
+ }
733
+ /** Pre-loads recorded outcomes so a replay never calls a real service. */
734
+ seed(records) {
735
+ for (const record of records) this.provide(record.src, record.outcome);
736
+ }
737
+ reset() {
738
+ this.queued = /* @__PURE__ */ new Map();
739
+ this.waiting = /* @__PURE__ */ new Map();
740
+ }
741
+ };
742
+ /**
743
+ * Builds the stub {@link ActorLogic} that replaces an invoke source named
744
+ * `src`, resolving its outcome from `registry` — the registry of the campaign
745
+ * that built the stub. One stub is built per campaign, so concurrent campaigns
746
+ * never resolve each other's outcomes.
747
+ *
748
+ * `registry` is `undefined` for stubs built only so a machine missing an
749
+ * implementation can be provided for pure-mode traversal, which steps logic
750
+ * without running it; a run that starts such a stub is a bug.
751
+ */
752
+ const createOutcomeStub = dual(2, function createOutcomeStub(src, registry) {
753
+ return createAsyncLogic({ run: () => Effect.runPromise(Effect.gen(function* () {
754
+ if (registry === void 0) throw new Error(`Property outcome stub for "${src}" ran outside an executed-mode property run`);
755
+ const outcome = yield* Effect.promise(() => registry.request(src));
756
+ if (outcome.ok) return outcome.output;
757
+ throw outcome.error;
758
+ })) });
759
+ });
760
+ /** Applies `actors` to a machine, rejecting logic that cannot be provided. */
761
+ const provideActors = dual(2, function provideActors(logic, actors) {
762
+ const provide = logic.provide;
763
+ if (typeof provide !== "function") throw new Error("Property `actors` and `outcomes` require a state machine; the provided actor logic has no `provide()`");
764
+ return provide.call(logic, { actors });
765
+ });
766
+ // src/engine/report.ts
767
+ /**
768
+ * Writes each log message to the console unchanged. The reports this package
769
+ * prints are its output contract, so logging through `Effect.log` with this
770
+ * logger keeps the exact lines callers (and the test suite) rely on.
771
+ */
772
+ const consoleLineLogger = Logger.withConsoleLog(Logger.make(({ message }) => String(message)));
773
+ /**
774
+ * Tells the data-first form of a coverage formatter from its data-last form: a
775
+ * one-argument call is data-first only when it passes a coverage, the only
776
+ * argument shape that carries `eventCases`.
777
+ */
778
+ const isCoverageDataFirst = (args) => args.length >= 2 || typeof args[0] === "object" && args[0] !== null && "eventCases" in args[0];
779
+ const DIMENSION_KEYS = [
780
+ "states",
781
+ "stateNodes",
782
+ "configurations",
783
+ "statuses",
784
+ "eventTypes",
785
+ "transitions",
786
+ "transitionPairs",
787
+ "requirements",
788
+ "frontiers"
789
+ ];
790
+ /**
791
+ * Coverage declares a `(runtime …)` placeholder in the dimensions whose
792
+ * universe is only known at runtime (serialized states, configurations). It
793
+ * marks the universe as open-ended; it is not an item, so reports leave it out
794
+ * of the counts and lists.
795
+ */
796
+ function isRuntimePlaceholder(id) {
797
+ return id.startsWith("(runtime ");
798
+ }
799
+ function getDimension(coverage, key) {
800
+ const dimension = coverage[key];
801
+ if (!dimension.unknown.some(isRuntimePlaceholder)) return dimension;
802
+ return {
803
+ ...dimension,
804
+ unknown: dimension.unknown.filter((id) => !isRuntimePlaceholder(id))
805
+ };
806
+ }
807
+ function totalOf(dimension) {
808
+ return dimension.covered.length + dimension.uncovered.length + dimension.unreachable.length + dimension.unknown.length;
809
+ }
810
+ function percentage(covered, total) {
811
+ if (total === 0) return "100.0%";
812
+ return `${(covered / total * 100).toFixed(1)}%`;
813
+ }
814
+ /**
815
+ * Renders a coverage id in a human-readable form. Ids are stable JSON strings;
816
+ * ids that are not recognized are rendered as-is.
817
+ * @experimental
818
+ */
819
+ function formatTestCoverageId(id) {
820
+ let parsed;
821
+ try {
822
+ parsed = JSON.parse(id);
823
+ } catch {
824
+ return id;
825
+ }
826
+ if (!Array.isArray(parsed)) return id;
827
+ const [kind, ...rest] = parsed;
828
+ if (kind === "transition" && rest.length === 3) {
829
+ const [source, eventType, index] = rest;
830
+ return `${source} --${eventType}--> #${String(index)}`;
831
+ }
832
+ if (kind === "event-case" && rest.length === 2) {
833
+ const [eventType, caseName] = rest;
834
+ return `${eventType} / ${caseName}`;
835
+ }
836
+ return id;
837
+ }
838
+ function dimensionLine(name, dimension) {
839
+ const total = totalOf(dimension);
840
+ const covered = dimension.covered.length;
841
+ return `${name}: ${covered}/${total} covered (${percentage(covered, total)}), ${dimension.uncovered.length} uncovered, ${dimension.unreachable.length} unreachable, ${dimension.unknown.length} unknown`;
842
+ }
843
+ function explorationLines(exploration) {
844
+ const lines = [
845
+ `runs: configured ${exploration.configuredRuns ?? "n/a"}, completed ${exploration.completedRuns}, attempted ${exploration.attemptedRuns}` + (exploration.shrinkRuns !== 0 ? `, shrinking ${exploration.shrinkRuns}` : ""),
846
+ `sequence length: max ${exploration.maximumSequenceLength ?? "n/a"}, max observed ${exploration.maximumObservedSequenceLength}`,
847
+ `stopped because: ${exploration.stoppedBecause}`,
848
+ ...exploration.swarm !== null ? [`swarm: ${exploration.swarm.runs} runs, ${exploration.swarm.averageEnabled.toFixed(2)} cases enabled on average`] : [],
849
+ ...exploration.target.improvements !== 0 ? [`target: best ${exploration.target.best}${exploration.target.label !== void 0 && exploration.target.label.length > 0 ? ` (${exploration.target.label})` : ""}, ${exploration.target.improvements} improvements`] : [],
850
+ ...exploration.truncated ? [`truncated: true${exploration.truncationReasons.length !== 0 ? ` (${[...exploration.truncationReasons].sort().join(", ")})` : ""}`] : [],
851
+ ...exploration.pendingActorSteps !== 0 ? [`pending actors: ${exploration.pendingActorSteps} step(s) settled with actor work in flight`] : []
852
+ ];
853
+ for (const frontier of exploration.frontiers) lines.push(`frontier ${frontier.id}: prefix ${frontier.prefixLength}, budget ${frontier.runBudget ?? "n/a"}, configured ${frontier.configuredRuns ?? "n/a"}, completed ${frontier.completedRuns}, attempted ${frontier.attemptedRuns}`);
854
+ for (const seed of exploration.seeds) lines.push(`seed ${seed.frontierId}: engine ${seed.engine ?? "n/a"}, seed ${seed.seed ?? "n/a"}, path ${seed.path ?? "n/a"}`);
855
+ return lines;
856
+ }
857
+ /** One line per temporal property with its run counts, then the warnings. */
858
+ function temporalLines(coverage, indent) {
859
+ return [...Object.entries(coverage.temporal.counts).sort(([left], [right]) => left.localeCompare(right)).map(([id, counts]) => `${indent}- ${id}: ${counts.satisfied} satisfied, ${counts.failed} failed, ${counts.inconclusive} inconclusive${coverage.temporal.failed.includes(id) && counts.failed === 0 ? " (never held)" : ""}`), ...coverage.temporal.warnings.map((warning) => `${indent}warning: ${warning}`)];
860
+ }
861
+ function listLines(title, ids) {
862
+ if (ids.length === 0) return [];
863
+ return [`${title}:`, ...[...ids].sort().map((id) => ` - ${formatTestCoverageId(id)}`)];
864
+ }
865
+ function markdownTable(headers, rows) {
866
+ return [
867
+ `| ${headers.join(" | ")} |`,
868
+ `| ${headers.map(() => "---").join(" | ")} |`,
869
+ ...rows.map((row) => `| ${row.join(" | ")} |`)
870
+ ];
871
+ }
872
+ function formatText(coverage) {
873
+ const lines = ["Test coverage", ""];
874
+ for (const key of DIMENSION_KEYS) lines.push(dimensionLine(key, getDimension(coverage, key)));
875
+ lines.push("");
876
+ for (const key of DIMENSION_KEYS) {
877
+ const dimension = getDimension(coverage, key);
878
+ lines.push(...listLines(`uncovered ${key}`, dimension.uncovered), ...listLines(`unreachable ${key}`, dimension.unreachable), ...listLines(`unknown ${key}`, dimension.unknown));
879
+ }
880
+ const eventCases = Object.entries(coverage.eventCases).sort(([left], [right]) => left.localeCompare(right));
881
+ if (eventCases.length !== 0) {
882
+ lines.push("event cases:");
883
+ for (const [id, counts] of eventCases) lines.push(` - ${formatTestCoverageId(id)}: ${counts.generated} generated, ${counts.applicable} applicable, ${counts.executed} executed, ${counts.ignored} ignored`);
884
+ }
885
+ const labels = Object.entries(coverage.labels).sort(([left], [right]) => left.localeCompare(right));
886
+ if (labels.length !== 0) {
887
+ lines.push("labels:");
888
+ for (const [name, label] of labels) {
889
+ const values = Object.entries(label.values).sort(([left], [right]) => left.localeCompare(right)).map(([value, count]) => `${value}=${count}`).join(", ");
890
+ lines.push(` - ${name}: ${label.count} recorded, ${percentage(Math.round(label.share * 1e3), 1e3)} of runs${values.length > 0 ? ` (${values})` : ""}`);
891
+ }
892
+ }
893
+ lines.push(`temporal: ${coverage.temporal.satisfied.length} satisfied, ${coverage.temporal.failed.length} failed, ${coverage.temporal.inconclusive.length} inconclusive`, ...temporalLines(coverage, " "), "", "exploration:", ...explorationLines(coverage.exploration).map((line) => ` ${line}`));
894
+ return lines.join("\n");
895
+ }
896
+ function formatMarkdown(coverage) {
897
+ const lines = ["# Test coverage", ""];
898
+ lines.push(...markdownTable([
899
+ "Dimension",
900
+ "Covered",
901
+ "Total",
902
+ "Ratio",
903
+ "Uncovered",
904
+ "Unreachable",
905
+ "Unknown"
906
+ ], DIMENSION_KEYS.map((key) => {
907
+ const dimension = getDimension(coverage, key);
908
+ const total = totalOf(dimension);
909
+ return [
910
+ key,
911
+ String(dimension.covered.length),
912
+ String(total),
913
+ percentage(dimension.covered.length, total),
914
+ String(dimension.uncovered.length),
915
+ String(dimension.unreachable.length),
916
+ String(dimension.unknown.length)
917
+ ];
918
+ })), "");
919
+ const outstanding = [];
920
+ for (const key of DIMENSION_KEYS) {
921
+ const dimension = getDimension(coverage, key);
922
+ for (const status of [
923
+ "uncovered",
924
+ "unreachable",
925
+ "unknown"
926
+ ]) for (const id of [...dimension[status]].sort()) outstanding.push([
927
+ key,
928
+ status,
929
+ formatTestCoverageId(id)
930
+ ]);
931
+ }
932
+ lines.push("## Outstanding", "");
933
+ if (outstanding.length !== 0) lines.push(...markdownTable([
934
+ "Dimension",
935
+ "Status",
936
+ "Id"
937
+ ], outstanding), "");
938
+ else lines.push("Everything declared was covered.", "");
939
+ const eventCases = Object.entries(coverage.eventCases).sort(([left], [right]) => left.localeCompare(right));
940
+ if (eventCases.length !== 0) lines.push("## Event cases", "", ...markdownTable([
941
+ "Case",
942
+ "Generated",
943
+ "Applicable",
944
+ "Executed",
945
+ "Ignored"
946
+ ], eventCases.map(([id, counts]) => [
947
+ formatTestCoverageId(id),
948
+ String(counts.generated),
949
+ String(counts.applicable),
950
+ String(counts.executed),
951
+ String(counts.ignored)
952
+ ])), "");
953
+ const labels = Object.entries(coverage.labels).sort(([left], [right]) => left.localeCompare(right));
954
+ if (labels.length !== 0) lines.push("## Labels", "", ...markdownTable([
955
+ "Label",
956
+ "Count",
957
+ "Share",
958
+ "Values"
959
+ ], labels.map(([name, label]) => [
960
+ name,
961
+ String(label.count),
962
+ percentage(Math.round(label.share * 1e3), 1e3),
963
+ Object.entries(label.values).sort(([left], [right]) => left.localeCompare(right)).map(([value, count]) => `${value}=${count}`).join("; ") || "-"
964
+ ])), "");
965
+ lines.push("## Temporal", "", ...markdownTable([
966
+ "Status",
967
+ "Count",
968
+ "Ids"
969
+ ], [
970
+ "satisfied",
971
+ "failed",
972
+ "inconclusive"
973
+ ].map((status) => [
974
+ status,
975
+ String(coverage.temporal[status].length),
976
+ [...coverage.temporal[status]].sort().join("; ") || "-"
977
+ ])), "", ...Object.keys(coverage.temporal.counts).length !== 0 ? [...markdownTable([
978
+ "Property",
979
+ "Satisfied runs",
980
+ "Failed runs",
981
+ "Inconclusive runs"
982
+ ], Object.entries(coverage.temporal.counts).sort(([left], [right]) => left.localeCompare(right)).map(([id, counts]) => [
983
+ id,
984
+ String(counts.satisfied),
985
+ String(counts.failed),
986
+ String(counts.inconclusive)
987
+ ])), ""] : [], ...coverage.temporal.warnings.map((warning) => `> Warning: ${warning}`), ...coverage.temporal.warnings.length !== 0 ? [""] : [], "## Exploration", "", ...explorationLines(coverage.exploration).map((line) => `- ${line}`));
988
+ return lines.join("\n");
989
+ }
990
+ /**
991
+ * Formats a {@link TestCoverage} as human-readable text or markdown.
992
+ *
993
+ * @experimental
994
+ */
995
+ const formatTestCoverage = dual(isCoverageDataFirst, function formatTestCoverage(coverage, options = {}) {
996
+ return options.format === "markdown" ? formatMarkdown(coverage) : formatText(coverage);
997
+ });
998
+ function dimensionToJSON(dimension) {
999
+ const total = totalOf(dimension);
1000
+ const covered = dimension.covered.length;
1001
+ return {
1002
+ total,
1003
+ covered,
1004
+ ratio: total !== 0 ? covered / total : 1,
1005
+ counts: { ...dimension.counts },
1006
+ coveredIds: [...dimension.covered],
1007
+ uncovered: [...dimension.uncovered],
1008
+ unreachable: [...dimension.unreachable],
1009
+ unknown: [...dimension.unknown]
1010
+ };
1011
+ }
1012
+ /**
1013
+ * Converts a {@link TestCoverage} to stable, versioned, JSON-safe data.
1014
+ *
1015
+ * @experimental
1016
+ */
1017
+ function testCoverageToJSON(coverage) {
1018
+ const dimensions = {};
1019
+ for (const key of DIMENSION_KEYS) dimensions[key] = dimensionToJSON(getDimension(coverage, key));
1020
+ return {
1021
+ formatVersion: 1,
1022
+ totals: {
1023
+ runs: coverage.runs,
1024
+ steps: coverage.steps,
1025
+ skipped: coverage.skipped,
1026
+ prefixSteps: coverage.prefixSteps,
1027
+ generatedSteps: coverage.generatedSteps,
1028
+ invariantChecks: coverage.invariantChecks,
1029
+ temporalChecks: coverage.temporalChecks,
1030
+ clockAdvances: coverage.clockAdvances,
1031
+ checkpoints: coverage.checkpoints,
1032
+ stops: coverage.stops,
1033
+ sutComparisons: coverage.sutComparisons,
1034
+ oracleComparisons: coverage.oracleComparisons
1035
+ },
1036
+ dimensions,
1037
+ eventCases: Object.fromEntries(Object.entries(coverage.eventCases).sort(([left], [right]) => left.localeCompare(right)).map(([id, counts]) => [id, {
1038
+ weight: counts.weight,
1039
+ generated: counts.generated,
1040
+ applicable: counts.applicable,
1041
+ executed: counts.executed,
1042
+ ignored: counts.ignored
1043
+ }])),
1044
+ temporal: {
1045
+ satisfied: [...coverage.temporal.satisfied],
1046
+ failed: [...coverage.temporal.failed],
1047
+ inconclusive: [...coverage.temporal.inconclusive],
1048
+ counts: Object.fromEntries(Object.entries(coverage.temporal.counts).sort(([left], [right]) => left.localeCompare(right)).map(([id, counts]) => [id, { ...counts }])),
1049
+ warnings: [...coverage.temporal.warnings]
1050
+ },
1051
+ labels: Object.fromEntries(Object.entries(coverage.labels).sort(([left], [right]) => left.localeCompare(right)).map(([name, label]) => [name, {
1052
+ count: label.count,
1053
+ values: { ...label.values },
1054
+ share: label.share
1055
+ }])),
1056
+ exploration: {
1057
+ stoppedBecause: coverage.exploration.stoppedBecause,
1058
+ configuredRuns: coverage.exploration.configuredRuns,
1059
+ completedRuns: coverage.exploration.completedRuns,
1060
+ attemptedRuns: coverage.exploration.attemptedRuns,
1061
+ shrinkRuns: coverage.exploration.shrinkRuns,
1062
+ maximumSequenceLength: coverage.exploration.maximumSequenceLength,
1063
+ maximumObservedSequenceLength: coverage.exploration.maximumObservedSequenceLength,
1064
+ truncated: coverage.exploration.truncated,
1065
+ truncationReasons: [...coverage.exploration.truncationReasons],
1066
+ frontiers: coverage.exploration.frontiers.map((frontier) => ({
1067
+ id: frontier.id,
1068
+ prefixLength: frontier.prefixLength,
1069
+ runBudget: frontier.runBudget,
1070
+ configuredRuns: frontier.configuredRuns,
1071
+ completedRuns: frontier.completedRuns,
1072
+ attemptedRuns: frontier.attemptedRuns
1073
+ })),
1074
+ seeds: coverage.exploration.seeds.map((seed) => ({
1075
+ frontierId: seed.frontierId,
1076
+ engine: seed.engine ?? null,
1077
+ seed: seed.seed ?? null,
1078
+ path: seed.path ?? null
1079
+ })),
1080
+ ...coverage.exploration.swarm !== null ? { swarm: {
1081
+ runs: coverage.exploration.swarm.runs,
1082
+ averageEnabled: coverage.exploration.swarm.averageEnabled
1083
+ } } : {},
1084
+ ...coverage.exploration.target.improvements !== 0 ? { target: {
1085
+ best: coverage.exploration.target.best,
1086
+ label: coverage.exploration.target.label ?? null,
1087
+ improvements: coverage.exploration.target.improvements
1088
+ } } : {}
1089
+ }
1090
+ };
1091
+ }
1092
+ function escapeXML(value) {
1093
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1094
+ }
1095
+ /**
1096
+ * Formats a {@link TestCoverage} as JUnit XML, with one `<testcase>` per
1097
+ * transition and per state node.
1098
+ * @experimental
1099
+ */
1100
+ const formatTestCoverageJUnit = dual(isCoverageDataFirst, function formatTestCoverageJUnit(coverage, options = {}) {
1101
+ const suiteName = options.suiteName ?? "test-coverage";
1102
+ const cases = [];
1103
+ let tests = 0;
1104
+ let failures = 0;
1105
+ let skipped = 0;
1106
+ for (const key of ["transitions", "stateNodes"]) {
1107
+ const dimension = getDimension(coverage, key);
1108
+ const entries = [
1109
+ ...dimension.covered.map((id) => [id, "covered"]),
1110
+ ...dimension.uncovered.map((id) => [id, "uncovered"]),
1111
+ ...dimension.unreachable.map((id) => [id, "unreachable"]),
1112
+ ...dimension.unknown.map((id) => [id, "unknown"])
1113
+ ];
1114
+ entries.sort(([left], [right]) => left.localeCompare(right));
1115
+ for (const [id, status] of entries) {
1116
+ tests++;
1117
+ const name = escapeXML(formatTestCoverageId(id));
1118
+ if (status === "covered") cases.push(` <testcase classname="${key}" name="${name}" />`);
1119
+ else if (status === "uncovered") {
1120
+ failures++;
1121
+ cases.push(` <testcase classname="${key}" name="${name}">\n <failure message="uncovered" type="uncovered" />\n </testcase>`);
1122
+ } else {
1123
+ skipped++;
1124
+ cases.push(` <testcase classname="${key}" name="${name}">\n <skipped message="${status}" />\n </testcase>`);
1125
+ }
1126
+ }
1127
+ }
1128
+ const properties = explorationLines(coverage.exploration).map((line, index) => ` <property name="exploration.${index}" value="${escapeXML(line)}" />`);
1129
+ return [
1130
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1131
+ "<testsuites>",
1132
+ ` <testsuite name="${escapeXML(suiteName)}" tests="${tests}" failures="${failures}" skipped="${skipped}">`,
1133
+ " <properties>",
1134
+ ...properties,
1135
+ " </properties>",
1136
+ ...cases,
1137
+ " </testsuite>",
1138
+ "</testsuites>",
1139
+ ""
1140
+ ].join("\n");
1141
+ });
1142
+ function escapeHTML(value) {
1143
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1144
+ }
1145
+ /**
1146
+ * Formats a {@link TestCoverage} as a self-contained HTML document.
1147
+ *
1148
+ * @experimental
1149
+ */
1150
+ const formatTestCoverageHTML = dual(isCoverageDataFirst, function formatTestCoverageHTML(coverage, options = {}) {
1151
+ const title = options.title ?? "Test coverage";
1152
+ const cards = DIMENSION_KEYS.map((key) => {
1153
+ const dimension = getDimension(coverage, key);
1154
+ const total = totalOf(dimension);
1155
+ return `<div class="card"><h2>${escapeHTML(key)}</h2><p class="ratio">${dimension.covered.length}/${total} (${percentage(dimension.covered.length, total)})</p><p class="detail">${dimension.uncovered.length} uncovered, ${dimension.unreachable.length} unreachable, ${dimension.unknown.length} unknown</p></div>`;
1156
+ }).join("");
1157
+ const rows = [];
1158
+ for (const key of DIMENSION_KEYS) {
1159
+ const dimension = getDimension(coverage, key);
1160
+ for (const status of [
1161
+ "uncovered",
1162
+ "unreachable",
1163
+ "unknown"
1164
+ ]) for (const id of [...dimension[status]].sort()) rows.push(`<tr><td>${escapeHTML(key)}</td><td>${escapeHTML(status)}</td><td>${escapeHTML(formatTestCoverageId(id))}</td></tr>`);
1165
+ }
1166
+ const explorationItems = explorationLines(coverage.exploration).map((line) => `<li>${escapeHTML(line)}</li>`).join("");
1167
+ return [
1168
+ "<!doctype html>",
1169
+ "<html lang=\"en\">",
1170
+ "<head>",
1171
+ "<meta charset=\"utf-8\" />",
1172
+ `<title>${escapeHTML(title)}</title>`,
1173
+ "<style>",
1174
+ "body{font-family:system-ui,sans-serif;margin:2rem;color:#111;background:#fff}",
1175
+ ".cards{display:flex;flex-wrap:wrap;gap:1rem}",
1176
+ ".card{border:1px solid #ddd;border-radius:8px;padding:1rem;min-width:12rem}",
1177
+ ".card h2{font-size:.9rem;margin:0 0 .5rem;text-transform:uppercase}",
1178
+ ".ratio{font-size:1.4rem;margin:0}",
1179
+ ".detail{color:#555;font-size:.8rem;margin:.25rem 0 0}",
1180
+ "table{border-collapse:collapse;margin-top:1rem;width:100%}",
1181
+ "th,td{border:1px solid #ddd;padding:.4rem .6rem;text-align:left;font-size:.85rem}",
1182
+ "</style>",
1183
+ "</head>",
1184
+ "<body>",
1185
+ `<h1>${escapeHTML(title)}</h1>`,
1186
+ `<p>${coverage.runs} runs, ${coverage.steps} steps, ${coverage.invariantChecks} invariant checks</p>`,
1187
+ `<div class="cards">${cards}</div>`,
1188
+ "<h2>Outstanding</h2>",
1189
+ rows.length !== 0 ? `<table><thead><tr><th>Dimension</th><th>Status</th><th>Id</th></tr></thead><tbody>${rows.join("")}</tbody></table>` : "<p>Everything declared was covered.</p>",
1190
+ "<h2>Temporal</h2>",
1191
+ `<p>${coverage.temporal.satisfied.length} satisfied, ${coverage.temporal.failed.length} failed, ${coverage.temporal.inconclusive.length} inconclusive</p>`,
1192
+ ...coverage.temporal.warnings.map((warning) => `<p><strong>Warning:</strong> ${escapeHTML(warning)}</p>`),
1193
+ "<h2>Exploration</h2>",
1194
+ `<ul>${explorationItems}</ul>`,
1195
+ "</body>",
1196
+ "</html>",
1197
+ ""
1198
+ ].join("\n");
1199
+ });
1200
+ /**
1201
+ * Throws an `Error` containing the formatted coverage text when any dimension's
1202
+ * covered ratio — `covered / (covered + uncovered)` — is below its threshold.
1203
+ * Thresholds are ratios between `0` and `1`.
1204
+ * @experimental
1205
+ */
1206
+ const assertTestCoverage = dual(2, function assertTestCoverage(coverage, thresholds) {
1207
+ const failures = [];
1208
+ for (const key of DIMENSION_KEYS) {
1209
+ const threshold = thresholds[key];
1210
+ if (threshold === void 0) continue;
1211
+ const dimension = getDimension(coverage, key);
1212
+ const considered = dimension.covered.length + dimension.uncovered.length;
1213
+ if ((considered !== 0 ? dimension.covered.length / considered : 1) < threshold) failures.push(`${key}: ${percentage(dimension.covered.length, considered)} covered, below the ${percentage(threshold, 1)} threshold`);
1214
+ }
1215
+ if (failures.length !== 0) throw new Error(`Test coverage thresholds not met:\n${failures.map((failure) => ` - ${failure}`).join("\n")}\n\n${formatTestCoverage(coverage)}`);
1216
+ });
1217
+ function formatShare(share) {
1218
+ return `${(share * 100).toFixed(1)}%`.padStart(6);
1219
+ }
1220
+ /**
1221
+ * Formats the distribution of generated data: how executed events split
1222
+ * across event cases, and the share of runs that recorded each label. Shrink
1223
+ * attempts are not counted. Pass `statistics: true` to `propertyTest()` or
1224
+ * `testPaths()` to print it after a passing campaign, or a function,
1225
+ * `(report) => void | Promise<void>`, to receive the same string instead.
1226
+ * @experimental
1227
+ */
1228
+ function formatTestStatistics(coverage) {
1229
+ const runs = coverage.exploration.attemptedRuns - coverage.exploration.shrinkRuns;
1230
+ const lines = [`Test statistics (${runs} run${runs === 1 ? "" : "s"})`];
1231
+ const eventCases = Object.entries(coverage.eventCases).sort(([left], [right]) => left.localeCompare(right));
1232
+ const executed = eventCases.reduce((total, [, counts]) => total + counts.executed, 0);
1233
+ if (eventCases.length !== 0) {
1234
+ lines.push("", "event cases (share of executed events):");
1235
+ for (const [id, counts] of eventCases) lines.push(` ${formatShare(executed !== 0 ? counts.executed / executed : 0)} ${formatTestCoverageId(id)}: ${counts.executed} executed, ${counts.ignored} ignored`);
1236
+ }
1237
+ const labels = Object.entries(coverage.labels).sort(([left], [right]) => left.localeCompare(right));
1238
+ if (labels.length !== 0) {
1239
+ lines.push("", "labels (share of runs):");
1240
+ for (const [name, label] of labels) {
1241
+ const values = Object.entries(label.values).sort(([left], [right]) => left.localeCompare(right)).map(([value, count]) => `${value}=${count}`).join(", ");
1242
+ lines.push(` ${formatShare(label.share)} ${name}: ${label.count} recorded${values.length > 0 ? ` (${values})` : ""}`);
1243
+ }
1244
+ }
1245
+ return lines.join("\n");
1246
+ }
1247
+ // src/engine/propertyTest.ts
1248
+ /**
1249
+ * Awaits a temporal predicate, which may hold synchronously or as a promise.
1250
+ * A rejection stays the same error object when the effect is run.
1251
+ */
1252
+ const temporalPredicate = (predicate, context) => Effect.promise(() => Promise.resolve(predicate(context)));
1253
+ /**
1254
+ * Runs an oracle's comparison, which may hold synchronously or as a promise,
1255
+ * falling back to structural equivalence. A rejection stays the same error
1256
+ * object when the effect is run.
1257
+ */
1258
+ const oracleMatches = (equivalent, model, observed) => {
1259
+ const compare = equivalent;
1260
+ return compare === void 0 ? Effect.succeed(defaultEquivalent(model, observed)) : Effect.promise(() => Promise.resolve(compare(model, observed)));
1261
+ };
1262
+ function getCauseMessage(cause) {
1263
+ if (cause instanceof Error) return cause.message || cause.name;
1264
+ if (typeof cause === "string") return cause;
1265
+ }
1266
+ /**
1267
+ * Builds the full failure message before `Error` captures the stack. The
1268
+ * formatted trace is part of the message so reporters that only print
1269
+ * `error.stack` still show the counterexample (GH #3435).
1270
+ */
1271
+ function getPropertyFailureMessage(summary, trace, cause, replay, fixture, format, extras) {
1272
+ const causeMessage = getCauseMessage(cause);
1273
+ const lines = [causeMessage !== void 0 && causeMessage.length > 0 && !summary.includes(causeMessage) ? `${summary}: ${causeMessage}` : summary];
1274
+ const reproduce = [
1275
+ ...replay?.seed === void 0 ? [] : [`seed ${replay.seed}`],
1276
+ ...replay?.path === void 0 ? [] : [`path "${replay.path}"`],
1277
+ ...replay?.replayPath === void 0 ? [] : [`replayPath "${replay.replayPath}"`]
1278
+ ];
1279
+ if (reproduce.length !== 0) lines.push(`Reproduce: ${reproduce.join(", ")}`);
1280
+ if (fixture !== void 0) lines.push("Fixture: failure.fixture (replayTest)");
1281
+ if (replay !== void 0 && replay.numShrinks !== void 0 && replay.numShrinks !== 0) lines.push(`Shrunk ${replay.numShrinks} time(s)`);
1282
+ lines.push(...extras?.notes ?? []);
1283
+ try {
1284
+ lines.push("", formatTestTrace(trace, format));
1285
+ } catch {}
1286
+ if (extras?.report !== void 0 && extras.report.length > 0) lines.push("", extras.report);
1287
+ return lines.join("\n");
1288
+ }
1289
+ /**
1290
+ * Thrown by `propertyTest()`, `testPaths()`, and `replayTest()` when an
1291
+ * oracle fails. Carries the trace, a portable replay fixture, and the coverage
1292
+ * accumulated up to the failure.
1293
+ * @experimental
1294
+ */
1295
+ const ModelTestFailureBase = Data.TaggedError("@systemfsoftware/xstate-test/engine/propertyTest/ModelTestFailure");
1296
+ var ModelTestFailure = class extends ModelTestFailureBase {
1297
+ trace;
1298
+ cause;
1299
+ replay;
1300
+ fixture;
1301
+ coverage;
1302
+ format;
1303
+ extras;
1304
+ /**
1305
+ * `instanceof` narrows to the default type arguments rather than `any`, so
1306
+ * `error.trace` stays typed in a `catch` block.
1307
+ */
1308
+ static [Symbol.hasInstance](value) {
1309
+ return Function.prototype[Symbol.hasInstance].call(this, value);
1310
+ }
1311
+ /** The short message, without the formatted trace. */
1312
+ summary;
1313
+ constructor(summary, trace, cause, replay, fixture, coverage, format, extras) {
1314
+ super({ message: getPropertyFailureMessage(summary, trace, cause, replay, fixture, format, extras) });
1315
+ this.trace = trace;
1316
+ this.cause = cause;
1317
+ this.replay = replay;
1318
+ this.fixture = fixture;
1319
+ this.coverage = coverage;
1320
+ this.format = format;
1321
+ this.extras = extras;
1322
+ this.name = "ModelTestFailure";
1323
+ this.summary = summary;
1324
+ }
1325
+ };
1326
+ /**
1327
+ * Thrown when a campaign completes without a counterexample but a
1328
+ * campaign-level assertion does not hold: a `sometimes` property that held in
1329
+ * no run, or a `reachable` target that no run entered.
1330
+ * @experimental
1331
+ */
1332
+ const TestCampaignErrorBase = Data.TaggedError("@systemfsoftware/xstate-test/engine/propertyTest/TestCampaignError");
1333
+ var TestCampaignError = class extends TestCampaignErrorBase {
1334
+ failures;
1335
+ coverage;
1336
+ name = "TestCampaignError";
1337
+ constructor(failures, coverage) {
1338
+ super({ message: `Campaign assertions failed:\n${failures.map((failure) => ` - ${failure}`).join("\n")}` });
1339
+ this.failures = failures;
1340
+ this.coverage = coverage;
1341
+ }
1342
+ };
1343
+ /**
1344
+ * Structural, key-order insensitive deep equality used to compare model
1345
+ * projections against reference/SUT observations. Cycle-safe.
1346
+ * @experimental
1347
+ */
1348
+ const defaultEquivalent = dual(2, function defaultEquivalent(left, right) {
1349
+ return deepEqual(left, right, /* @__PURE__ */ new Map());
1350
+ });
1351
+ function deepEqual(left, right, visited) {
1352
+ if (Object.is(left, right)) return true;
1353
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return left === right;
1354
+ const seen = visited.get(left);
1355
+ if (seen?.has(right) === true) return true;
1356
+ if (seen !== void 0) seen.add(right);
1357
+ else visited.set(left, /* @__PURE__ */ new Set([right]));
1358
+ if (left instanceof Date || right instanceof Date) return left instanceof Date && right instanceof Date && left.getTime() === right.getTime();
1359
+ if (Array.isArray(left) || Array.isArray(right)) {
1360
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
1361
+ return left.length === right.length && left.every((value, index) => deepEqual(value, right[index], visited));
1362
+ }
1363
+ if (left instanceof Map || right instanceof Map) {
1364
+ if (!(left instanceof Map) || !(right instanceof Map)) return false;
1365
+ if (left.size !== right.size) return false;
1366
+ for (const [key, value] of left) if (!right.has(key) || !deepEqual(value, right.get(key), visited)) return false;
1367
+ return true;
1368
+ }
1369
+ if (left instanceof Set || right instanceof Set) {
1370
+ if (!(left instanceof Set) || !(right instanceof Set)) return false;
1371
+ if (left.size !== right.size) return false;
1372
+ for (const value of left) if (!right.has(value)) return false;
1373
+ return true;
1374
+ }
1375
+ const leftKeys = Object.keys(left);
1376
+ const rightKeys = Object.keys(right);
1377
+ if (leftKeys.length !== rightKeys.length) return false;
1378
+ return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && deepEqual(left[key], right[key], visited));
1379
+ }
1380
+ function assertEventPayload(payload, type, caseId) {
1381
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
1382
+ const location = caseId !== void 0 && caseId.length > 0 ? `Property event case ${caseId}` : `Property event "${type}"`;
1383
+ throw new Error(`${location} generated a non-object payload (${typeof payload === "string" ? JSON.stringify(payload) : String(payload)}). Event payloads must be plain objects; use a \`resolve\` function to map generated values onto an event payload.`);
1384
+ }
1385
+ }
1386
+ /** Microtask turns awaited per drain round. */
1387
+ const DRAIN_MICROTASKS = 8;
1388
+ /** Upper bound on drain rounds awaited before an executed step is settled. */
1389
+ const MAX_DRAIN_ROUNDS = 40;
1390
+ /** Consecutive rounds without new inspection events that settle a step. */
1391
+ const QUIET_DRAIN_ROUNDS = 2;
1392
+ /** `count` consecutive indices; a `const` loop counter for generator bodies. */
1393
+ const indexRange = (count) => Array.from({ length: count }, (_, index) => index);
1394
+ /** One drain turn: `setImmediate` and a zero timer alternate to flush both queues. */
1395
+ const drainMacrotaskTurn = (round) => round % 2 === 0 && typeof setImmediate === "function" ? Effect.callback((resume) => {
1396
+ setImmediate(() => resume(Effect.void));
1397
+ }) : Effect.sleep(1);
1398
+ /** Drains `count` microtask turns in order, so queued continuations settle. */
1399
+ const microtaskTurns = (count) => count === 0 ? Effect.void : Effect.promise(() => Promise.resolve()).pipe(Effect.andThen(microtaskTurns(count - 1)));
1400
+ /**
1401
+ * Records the observation a step produced on its timeline entry. The
1402
+ * published entry types freeze `observation` as readonly, so the write goes
1403
+ * through one place instead of at every step.
1404
+ */
1405
+ const setEntryObservation = (entry, observation) => {
1406
+ Object.assign(entry, { observation });
1407
+ };
1408
+ /** The modelled transition when no actor is executed. */
1409
+ const pureTransitionStep = (logic, coverage, previousSnapshot, event) => {
1410
+ const [snapshot, effects, selected] = transitionWithDetails(logic, previousSnapshot, event);
1411
+ return {
1412
+ snapshot,
1413
+ effects,
1414
+ transitionIds: recordPropertyTransitions(coverage, event, selected),
1415
+ drained: []
1416
+ };
1417
+ };
1418
+ /** Stops the executed actor and drains the transitions it leaves behind. */
1419
+ const stopExecutedStep = (engine, coverage) => Effect.gen(function* () {
1420
+ engine.stop();
1421
+ yield* Effect.promise(() => engine.drain());
1422
+ const drained = engine.consume(coverage);
1423
+ return {
1424
+ snapshot: engine.getSnapshot(),
1425
+ effects: [],
1426
+ transitionIds: [],
1427
+ drained
1428
+ };
1429
+ });
1430
+ /** Sends one event through the executed actor and attributes its transitions. */
1431
+ const sendExecutedStep = (engine, coverage, event) => Effect.gen(function* () {
1432
+ engine.send(event);
1433
+ yield* Effect.promise(() => engine.drain());
1434
+ const drained = engine.consume(coverage);
1435
+ const snapshot = engine.getSnapshot();
1436
+ const primaryIndex = drained.findIndex((entry) => entry.source === "root" && entry.event.type === event.type);
1437
+ const primary = primaryIndex === -1 ? void 0 : drained[primaryIndex];
1438
+ return {
1439
+ snapshot,
1440
+ effects: primary?.effects ?? [],
1441
+ transitionIds: primary?.transitionIds ?? [],
1442
+ drained: primaryIndex === -1 ? drained : drained.slice(primaryIndex + 1)
1443
+ };
1444
+ });
1445
+ /**
1446
+ * Drives a real actor on a `SimulatedClock` and turns its inspection
1447
+ * stream into the same transition details the pure path returns.
1448
+ */
1449
+ var PropertyExecutionEngine = class {
1450
+ stubbedSources;
1451
+ clock = new SimulatedClock();
1452
+ actor;
1453
+ buffer = [];
1454
+ rootRef;
1455
+ srcByActorId = /* @__PURE__ */ new Map();
1456
+ resolvedBySrc = /* @__PURE__ */ new Map();
1457
+ /** Outcomes observed this run, in resolution order. */
1458
+ outcomes = [];
1459
+ constructor(logic, input, startingSnapshot, stubbedSources = /* @__PURE__ */ new Set()) {
1460
+ this.stubbedSources = stubbedSources;
1461
+ const actor = createActor(logic, {
1462
+ clock: this.clock,
1463
+ input,
1464
+ ...startingSnapshot !== void 0 ? { snapshot: startingSnapshot } : {},
1465
+ inspect: (event) => {
1466
+ this.buffer.push(event);
1467
+ }
1468
+ });
1469
+ this.rootRef = actor;
1470
+ this.actor = actor;
1471
+ }
1472
+ start() {
1473
+ this.actor.start();
1474
+ }
1475
+ send(event) {
1476
+ this.actor.send(event);
1477
+ }
1478
+ advance(milliseconds) {
1479
+ this.clock.increment(milliseconds);
1480
+ }
1481
+ stop() {
1482
+ this.actor.stop();
1483
+ }
1484
+ getSnapshot() {
1485
+ return this.actor.getSnapshot();
1486
+ }
1487
+ /**
1488
+ * Runs microtasks and macrotasks until the inspection stream stops growing.
1489
+ * Rounds alternate between `setImmediate` and a zero `setTimeout`, so work
1490
+ * queued on either macrotask queue (a promise actor awaiting
1491
+ * `setTimeout(0)`, say) settles within the step that started it. While a
1492
+ * child actor has asynchronous work in flight, a step settles only after
1493
+ * {@link QUIET_DRAIN_ROUNDS} consecutive rounds without new inspection
1494
+ * events; otherwise one quiet round suffices. The actor's own timers are on
1495
+ * the simulated clock, so nothing here can fire a delayed transition.
1496
+ */
1497
+ drain() {
1498
+ return Effect.runPromise(Effect.gen(function* () {
1499
+ const quietRounds = yield* Ref.make(0);
1500
+ for (const round of indexRange(MAX_DRAIN_ROUNDS)) {
1501
+ const seen = this.buffer.length;
1502
+ yield* microtaskTurns(DRAIN_MICROTASKS);
1503
+ yield* drainMacrotaskTurn(round);
1504
+ yield* Ref.update(quietRounds, (value) => this.buffer.length === seen ? value + 1 : 0);
1505
+ const current = yield* Ref.get(quietRounds);
1506
+ if (current >= QUIET_DRAIN_ROUNDS || current > 0 && this.getPendingActors().length === 0) return;
1507
+ }
1508
+ }.bind(this)));
1509
+ }
1510
+ /**
1511
+ * Direct children of the tested actor whose asynchronous work (a promise
1512
+ * body, for instance) is still in flight. Stubbed invoke sources are left
1513
+ * out: they wait for an `outcome` command by design.
1514
+ */
1515
+ getPendingActors() {
1516
+ const children = this.actor.getSnapshot().children ?? {};
1517
+ const pending = [];
1518
+ for (const [id, child] of Object.entries(children)) {
1519
+ const src = this.srcByActorId.get(child?.id ?? id);
1520
+ if (src !== void 0 && this.stubbedSources.has(src)) continue;
1521
+ const snapshot = child?.getSnapshot?.();
1522
+ if (snapshot?.status !== "active" || snapshot.effects === void 0) continue;
1523
+ if (Object.entries(snapshot.effects).some(([key, effect]) => key !== "callback" && effect?.status === "active")) pending.push(id);
1524
+ }
1525
+ return pending.sort();
1526
+ }
1527
+ /**
1528
+ * The milliseconds until the pending `xstate.after` timer for `event` is
1529
+ * due on the simulated clock, or `undefined` when no such timer is
1530
+ * scheduled on the tested actor.
1531
+ */
1532
+ getAfterTimerRemaining(event) {
1533
+ const timers = this.actor.getSnapshot().timers ?? {};
1534
+ const timerId = Object.keys(timers).find((id) => {
1535
+ const timerEvent = timers[id]?.event;
1536
+ return timerEvent?.type === "xstate.after" && timerEvent.delay === event.delay && timerEvent.stateId === event.stateId;
1537
+ });
1538
+ if (timerId === void 0) return;
1539
+ const system = this.rootRef.system;
1540
+ const scheduled = Object.values(system?.getSnapshot?.()._scheduledTimers ?? {}).find((timer) => timer.source === this.rootRef && timer.id === timerId);
1541
+ if (scheduled === void 0) return;
1542
+ return Math.max(0, scheduled.dueAt - this.clock.now());
1543
+ }
1544
+ /**
1545
+ * Consumes the buffered inspection events, records root transitions into
1546
+ * `coverage`, and returns one {@link DrainedTransition} per transition.
1547
+ */
1548
+ consume(coverage) {
1549
+ const buffered = this.buffer;
1550
+ this.buffer = [];
1551
+ const drained = [];
1552
+ for (const inspected of buffered) {
1553
+ if (inspected.type === "@xstate.actor") {
1554
+ if (typeof inspected.src === "string") this.srcByActorId.set(inspected.id, inspected.src);
1555
+ continue;
1556
+ }
1557
+ if (inspected.type !== "@xstate.transition") continue;
1558
+ const isRoot = inspected.actorRef === this.rootRef;
1559
+ const actorId = inspected.actorRef?.id ?? "(unknown)";
1560
+ if (!isRoot) this.recordOutcome(actorId, inspected.snapshot);
1561
+ drained.push({
1562
+ source: isRoot ? "root" : "child",
1563
+ actorId,
1564
+ event: inspected.event,
1565
+ snapshot: inspected.snapshot,
1566
+ effects: inspected.actions,
1567
+ transitionIds: isRoot ? recordPropertyTransitions(coverage, inspected.event, inspected.microsteps) : []
1568
+ });
1569
+ }
1570
+ return drained;
1571
+ }
1572
+ recordOutcome(actorId, snapshot) {
1573
+ if (snapshot.status !== "done" && snapshot.status !== "error") return;
1574
+ const src = this.srcByActorId.get(actorId);
1575
+ if (src === void 0) return;
1576
+ const occurrence = this.resolvedBySrc.get(src) ?? 0;
1577
+ this.resolvedBySrc.set(src, occurrence + 1);
1578
+ this.outcomes.push({
1579
+ src,
1580
+ occurrence,
1581
+ outcome: snapshot.status === "done" ? {
1582
+ ok: true,
1583
+ output: snapshot.output
1584
+ } : {
1585
+ ok: false,
1586
+ error: snapshot.error
1587
+ }
1588
+ });
1589
+ }
1590
+ };
1591
+ /** @experimental */
1592
+ var PropertyScenarioRunner = class {
1593
+ logic;
1594
+ input;
1595
+ startingSnapshot;
1596
+ serializeStartingSnapshot;
1597
+ prefixEvents;
1598
+ frontierId;
1599
+ sut;
1600
+ states;
1601
+ reference;
1602
+ invariant;
1603
+ eventDescriptors;
1604
+ coverage;
1605
+ executionConfig;
1606
+ snapshot;
1607
+ initialSnapshot;
1608
+ initialEffects = [];
1609
+ initialTransitionIds = [];
1610
+ timeline = [];
1611
+ temporal;
1612
+ stableStep = 0;
1613
+ started = false;
1614
+ finished = false;
1615
+ sutSession;
1616
+ referenceSession;
1617
+ lastObservation;
1618
+ inconclusiveTemporalIds = [];
1619
+ generatedCommandCount = 0;
1620
+ labelsSeen = /* @__PURE__ */ new Set();
1621
+ execution;
1622
+ swarmCaseIds;
1623
+ swarmEnabled;
1624
+ targetFunction;
1625
+ targetObservations = [];
1626
+ /** Set for runs the adapter starts while shrinking a counterexample. */
1627
+ shrinkRun = false;
1628
+ /** The failure this run ended in, if an oracle failed. */
1629
+ failure;
1630
+ /** Records a label for this run. */
1631
+ label = (name, value) => {
1632
+ if (this.shrinkRun) return;
1633
+ recordPropertyLabel(this.coverage, name, value, this.labelsSeen);
1634
+ };
1635
+ /**
1636
+ * Marks this run as a shrink attempt: its labels and event cases are left
1637
+ * out of the campaign's statistics.
1638
+ */
1639
+ markShrinkRun() {
1640
+ this.shrinkRun = true;
1641
+ }
1642
+ recordEventCase(caseId, stage) {
1643
+ if (!this.shrinkRun) recordPropertyEventCase(this.coverage, caseId, stage);
1644
+ }
1645
+ /** Records `name` when `condition` holds. */
1646
+ classify = (condition, name) => {
1647
+ if (condition) this.label(name);
1648
+ };
1649
+ /** Records an observation for targeted search. */
1650
+ target = (observation, label) => {
1651
+ if (typeof observation !== "number" || Number.isNaN(observation)) throw new Error("target() requires a numeric observation");
1652
+ this.targetObservations.push({
1653
+ value: observation,
1654
+ label,
1655
+ index: this.timeline.length
1656
+ });
1657
+ };
1658
+ /**
1659
+ * Restricts this run to a subset of the declared event cases. Every other
1660
+ * case reports itself as inapplicable, so a run only exercises the enabled
1661
+ * ones. See the `swarm` option.
1662
+ */
1663
+ setSwarm(caseIds) {
1664
+ this.swarmCaseIds = caseIds;
1665
+ this.swarmEnabled = new Set(caseIds);
1666
+ }
1667
+ formatOptions;
1668
+ /** Sets how failure messages render snapshots. See `formatSnapshot`. */
1669
+ setFormatSnapshot(formatSnapshot) {
1670
+ this.formatOptions = formatSnapshot !== void 0 ? { formatSnapshot } : void 0;
1671
+ }
1672
+ /** Evaluates `target` on every stable step. See the `target` option. */
1673
+ setTargetFunction(targetFunction) {
1674
+ this.targetFunction = targetFunction;
1675
+ }
1676
+ /** Outcomes observed while the executed run was alive. */
1677
+ executionOutcomes;
1678
+ /** Observations recorded with `target()` during this run. */
1679
+ getTargetObservations() {
1680
+ return this.targetObservations;
1681
+ }
1682
+ /** `true` when the run completed without a property failure. */
1683
+ isFinished() {
1684
+ return this.finished;
1685
+ }
1686
+ constructor(logic, input, startingSnapshot, serializeStartingSnapshot, prefixEvents, frontierId, sut, states, reference, invariant, temporal, eventDescriptors, coverage, executionConfig) {
1687
+ this.logic = logic;
1688
+ this.input = input;
1689
+ this.startingSnapshot = startingSnapshot;
1690
+ this.serializeStartingSnapshot = serializeStartingSnapshot;
1691
+ this.prefixEvents = prefixEvents;
1692
+ this.frontierId = frontierId;
1693
+ this.sut = sut;
1694
+ this.states = states;
1695
+ this.reference = reference;
1696
+ this.invariant = invariant;
1697
+ this.eventDescriptors = eventDescriptors;
1698
+ this.coverage = coverage;
1699
+ this.executionConfig = executionConfig;
1700
+ this.temporal = temporal.map((definition) => ({
1701
+ definition,
1702
+ satisfied: false
1703
+ }));
1704
+ }
1705
+ start() {
1706
+ return Effect.runPromise(Effect.gen(function* () {
1707
+ resetPropertyTransitionPairs(this.coverage);
1708
+ const [initial, effects, selected] = this.startingSnapshot !== void 0 ? [
1709
+ this.startingSnapshot,
1710
+ [],
1711
+ []
1712
+ ] : initialTransitionWithDetails(this.logic, this.input);
1713
+ const snapshot = initial;
1714
+ this.initialTransitionIds = recordPropertyTransitions(this.coverage, { type: XSTATE_INIT }, selected);
1715
+ const initialSnapshot = yield* Ref.make(snapshot);
1716
+ const initialEffects = yield* Ref.make(effects);
1717
+ const executionConfig = this.executionConfig;
1718
+ if (executionConfig !== void 0 && executionConfig.mode === "executed") {
1719
+ executionConfig.registry.reset();
1720
+ executionConfig.registry.seed(executionConfig.seededOutcomes ?? []);
1721
+ const execution = new PropertyExecutionEngine(this.logic, this.input, this.startingSnapshot, new Set(executionConfig.stubbedSources ?? []));
1722
+ this.execution = execution;
1723
+ execution.start();
1724
+ yield* Effect.promise(() => execution.drain());
1725
+ execution.consume(this.coverage);
1726
+ yield* Ref.set(initialSnapshot, execution.getSnapshot());
1727
+ yield* Ref.set(initialEffects, []);
1728
+ }
1729
+ const finalSnapshot = yield* Ref.get(initialSnapshot);
1730
+ const finalEffects = yield* Ref.get(initialEffects);
1731
+ this.snapshot = finalSnapshot;
1732
+ this.initialSnapshot = finalSnapshot;
1733
+ this.initialEffects = finalEffects;
1734
+ this.started = true;
1735
+ this.recordSnapshot(finalSnapshot);
1736
+ const context = {
1737
+ logic: this.logic,
1738
+ input: this.input,
1739
+ snapshot: this.startingSnapshot,
1740
+ label: this.label,
1741
+ classify: this.classify,
1742
+ target: this.target
1743
+ };
1744
+ const reference = this.reference;
1745
+ if (reference !== void 0) this.referenceSession = yield* Effect.promise(() => Promise.resolve(reference.create(context)));
1746
+ const sut = this.sut;
1747
+ if (sut !== void 0) this.sutSession = yield* Effect.promise(() => Promise.resolve(sut.create(context)));
1748
+ yield* this.checkStable(void 0, finalSnapshot, finalSnapshot, finalEffects);
1749
+ for (const event of this.prefixEvents) yield* this.executeEvent(event, "prefix", "frontier", true);
1750
+ if (this.frontierId !== void 0 && this.frontierId.length > 0) incrementCoverage(this.coverage.frontiers, this.frontierId);
1751
+ }.bind(this)));
1752
+ }
1753
+ canRun(event, caseId) {
1754
+ return this.canRunResolved(event, caseId);
1755
+ }
1756
+ canRunGenerated(type, generated, caseId) {
1757
+ return this.canRunResolved(this.resolveGeneratedEvent(type, generated, caseId), caseId);
1758
+ }
1759
+ canRunResolved(event, caseId) {
1760
+ this.recordEventCase(caseId, "generated");
1761
+ const descriptor = this.eventDescriptors.get(caseId);
1762
+ const canRun = (this.swarmEnabled?.has(caseId) ?? true) && event !== void 0 && this.snapshot.status === "active" && (descriptor?.when?.({
1763
+ snapshot: this.snapshot,
1764
+ event
1765
+ }) ?? true);
1766
+ if (!canRun) {
1767
+ this.coverage.skipped++;
1768
+ this.recordEventCase(caseId, "ignored");
1769
+ } else this.recordEventCase(caseId, "applicable");
1770
+ return canRun;
1771
+ }
1772
+ resolveGeneratedEvent(type, generated, caseId) {
1773
+ const descriptor = this.eventDescriptors.get(caseId);
1774
+ const payload = descriptor !== void 0 && "resolve" in descriptor && descriptor.resolve !== void 0 ? descriptor.resolve({
1775
+ snapshot: this.snapshot,
1776
+ generated
1777
+ }) : generated;
1778
+ if (payload === void 0) return;
1779
+ assertEventPayload(payload, type, caseId);
1780
+ return {
1781
+ ...payload,
1782
+ type
1783
+ };
1784
+ }
1785
+ /** `outcome` commands only apply while the executed actor is running. */
1786
+ canRunOutcome() {
1787
+ return this.canRunCommand(this.execution !== void 0 && this.snapshot.status === "active");
1788
+ }
1789
+ canRunCommand(applicable) {
1790
+ if (!applicable) this.coverage.skipped++;
1791
+ return applicable;
1792
+ }
1793
+ run(event, caseId) {
1794
+ return Effect.runPromise(Effect.gen(function* () {
1795
+ this.assertStarted();
1796
+ this.recordGeneratedCommand();
1797
+ this.recordEventCase(caseId, "executed");
1798
+ yield* this.executeEvent(event, "generated", "generator", true, true, caseId);
1799
+ }.bind(this)));
1800
+ }
1801
+ runGenerated(type, generated, caseId) {
1802
+ return Effect.runPromise(Effect.gen(function* () {
1803
+ const event = this.resolveGeneratedEvent(type, generated, caseId);
1804
+ if (event === void 0) throw new Error(`Property event case ${caseId} became inapplicable before execution`);
1805
+ yield* Effect.promise(() => this.run(event, caseId));
1806
+ }.bind(this)));
1807
+ }
1808
+ replay(command) {
1809
+ return Effect.runPromise(Effect.gen(function* () {
1810
+ this.assertStarted();
1811
+ if (command.type === "event") yield* this.executeEvent(command.event, command.phase, command.origin, command.origin !== "clock", true, command.caseId);
1812
+ else if (command.type === "advance") {
1813
+ if (this.execution !== void 0) {
1814
+ yield* Effect.promise(() => this.advanceExecuted(command.milliseconds));
1815
+ return;
1816
+ }
1817
+ this.coverage.clockAdvances++;
1818
+ this.timeline.push({
1819
+ kind: "command",
1820
+ index: this.timeline.length,
1821
+ command,
1822
+ previousSnapshot: this.snapshot,
1823
+ snapshot: this.snapshot,
1824
+ effects: [],
1825
+ transitionIds: []
1826
+ });
1827
+ } else if (command.type === "outcome") {
1828
+ if (this.execution === void 0) throw new Error(`Property replay fixture contains an \`outcome\` command for "${command.src}" but the replay is running in pure mode: the fixture was recorded in executed mode; pass mode: 'executed'`);
1829
+ yield* Effect.promise(() => this.outcome(command.src, command.outcome));
1830
+ } else if (command.type === "checkpoint") yield* Effect.promise(() => this.checkpoint(command.label));
1831
+ else yield* Effect.promise(() => this.stop());
1832
+ }.bind(this)));
1833
+ }
1834
+ /** Queues the next resolution of the stubbed invoke source `src`. */
1835
+ outcome(src, outcome) {
1836
+ return Effect.runPromise(Effect.gen(function* () {
1837
+ this.assertStarted();
1838
+ this.recordGeneratedCommand();
1839
+ const execution = this.execution;
1840
+ const executionConfig = this.executionConfig;
1841
+ if (execution === void 0 || executionConfig === void 0) throw new Error("Property `outcome` commands require `mode: 'executed'`");
1842
+ const previousSnapshot = this.snapshot;
1843
+ executionConfig.registry.provide(src, outcome);
1844
+ yield* Effect.promise(() => execution.drain());
1845
+ const drained = execution.consume(this.coverage);
1846
+ this.snapshot = execution.getSnapshot();
1847
+ this.recordSnapshot(this.snapshot);
1848
+ const entry = {
1849
+ kind: "command",
1850
+ index: this.timeline.length,
1851
+ command: {
1852
+ type: "outcome",
1853
+ src,
1854
+ outcome
1855
+ },
1856
+ previousSnapshot,
1857
+ snapshot: this.snapshot,
1858
+ effects: [],
1859
+ transitionIds: []
1860
+ };
1861
+ this.timeline.push(entry);
1862
+ this.markPendingActors(entry);
1863
+ this.pushActorEntries(previousSnapshot, drained);
1864
+ const observation = yield* this.checkStable(void 0, previousSnapshot, this.snapshot, []);
1865
+ setEntryObservation(entry, observation);
1866
+ }.bind(this)));
1867
+ }
1868
+ advance(milliseconds) {
1869
+ return Effect.runPromise(Effect.gen(function* () {
1870
+ this.assertStarted();
1871
+ this.recordGeneratedCommand();
1872
+ if (this.execution !== void 0) {
1873
+ yield* Effect.promise(() => this.advanceExecuted(milliseconds));
1874
+ return;
1875
+ }
1876
+ const sutSession = this.sutSession;
1877
+ if (sutSession?.advance === void 0) {
1878
+ const advancedFrom = this.snapshot;
1879
+ this.coverage.clockAdvances++;
1880
+ const pureEntry = {
1881
+ kind: "command",
1882
+ index: this.timeline.length,
1883
+ command: {
1884
+ type: "advance",
1885
+ milliseconds,
1886
+ deliveredEvents: []
1887
+ },
1888
+ previousSnapshot: advancedFrom,
1889
+ snapshot: this.snapshot,
1890
+ effects: [],
1891
+ transitionIds: []
1892
+ };
1893
+ this.timeline.push(pureEntry);
1894
+ const pureObservation = yield* this.checkStable(void 0, advancedFrom, this.snapshot, []);
1895
+ setEntryObservation(pureEntry, pureObservation);
1896
+ return;
1897
+ }
1898
+ const previousSnapshot = this.snapshot;
1899
+ const advance = sutSession.advance.bind(sutSession);
1900
+ const events = yield* Effect.promise(() => Promise.resolve(advance(milliseconds)));
1901
+ const command = {
1902
+ type: "advance",
1903
+ milliseconds,
1904
+ deliveredEvents: events.slice()
1905
+ };
1906
+ this.coverage.clockAdvances++;
1907
+ this.timeline.push({
1908
+ kind: "command",
1909
+ index: this.timeline.length,
1910
+ command,
1911
+ previousSnapshot,
1912
+ snapshot: this.snapshot,
1913
+ effects: [],
1914
+ transitionIds: []
1915
+ });
1916
+ for (const [index, event] of events.entries()) yield* this.executeEvent(event, "generated", "clock", false, index === events.length - 1);
1917
+ if (events.length === 0) {
1918
+ const observation = yield* this.compareObservations();
1919
+ this.lastObservation = observation;
1920
+ this.replaceLastObservation(observation);
1921
+ }
1922
+ }.bind(this)));
1923
+ }
1924
+ /**
1925
+ * Advances the simulated clock the executed actor runs on, then drains
1926
+ * whatever the elapsed delays produced.
1927
+ */
1928
+ advanceExecuted(milliseconds) {
1929
+ return Effect.runPromise(Effect.gen(function* () {
1930
+ const execution = this.execution;
1931
+ if (execution === void 0) return;
1932
+ const previousSnapshot = this.snapshot;
1933
+ execution.advance(milliseconds);
1934
+ yield* Effect.promise(() => execution.drain());
1935
+ const drained = execution.consume(this.coverage);
1936
+ this.snapshot = execution.getSnapshot();
1937
+ this.recordSnapshot(this.snapshot);
1938
+ this.coverage.clockAdvances++;
1939
+ const entry = {
1940
+ kind: "command",
1941
+ index: this.timeline.length,
1942
+ command: {
1943
+ type: "advance",
1944
+ milliseconds,
1945
+ deliveredEvents: []
1946
+ },
1947
+ previousSnapshot,
1948
+ snapshot: this.snapshot,
1949
+ effects: [],
1950
+ transitionIds: []
1951
+ };
1952
+ this.timeline.push(entry);
1953
+ this.markPendingActors(entry);
1954
+ this.pushActorEntries(previousSnapshot, drained);
1955
+ const observation = yield* this.checkStable(void 0, previousSnapshot, this.snapshot, []);
1956
+ setEntryObservation(entry, observation);
1957
+ }.bind(this)));
1958
+ }
1959
+ checkpoint(label) {
1960
+ return Effect.runPromise(Effect.gen(function* () {
1961
+ this.assertStarted();
1962
+ this.recordGeneratedCommand();
1963
+ const entry = {
1964
+ kind: "command",
1965
+ index: this.timeline.length,
1966
+ command: {
1967
+ type: "checkpoint",
1968
+ label
1969
+ },
1970
+ previousSnapshot: this.snapshot,
1971
+ snapshot: this.snapshot,
1972
+ effects: [],
1973
+ transitionIds: []
1974
+ };
1975
+ this.timeline.push(entry);
1976
+ yield* Effect.promise(() => Promise.resolve(this.sutSession?.checkpoint?.(label)));
1977
+ this.coverage.checkpoints++;
1978
+ const observation = yield* this.compareObservations();
1979
+ this.lastObservation = observation;
1980
+ setEntryObservation(entry, observation);
1981
+ }.bind(this)));
1982
+ }
1983
+ stop() {
1984
+ return Effect.runPromise(Effect.gen(function* () {
1985
+ this.assertStarted();
1986
+ this.recordGeneratedCommand();
1987
+ const previousSnapshot = this.snapshot;
1988
+ const execution = this.execution;
1989
+ const stopEvent = { type: XSTATE_STOP };
1990
+ const { snapshot, effects, transitionIds, drained } = yield* execution !== void 0 ? stopExecutedStep(execution, this.coverage) : Effect.succeed(pureTransitionStep(this.logic, this.coverage, previousSnapshot, stopEvent));
1991
+ this.snapshot = snapshot;
1992
+ yield* Effect.promise(() => Promise.resolve(this.referenceSession?.stop?.()));
1993
+ yield* Effect.promise(() => Promise.resolve(this.sutSession?.stop?.()));
1994
+ this.coverage.stops++;
1995
+ this.coverage.steps++;
1996
+ this.coverage.generatedSteps++;
1997
+ this.recordSnapshot(snapshot);
1998
+ const entry = {
1999
+ kind: "command",
2000
+ index: this.timeline.length,
2001
+ command: { type: "stop" },
2002
+ previousSnapshot,
2003
+ snapshot,
2004
+ effects,
2005
+ transitionIds
2006
+ };
2007
+ this.timeline.push(entry);
2008
+ this.markPendingActors(entry);
2009
+ this.pushActorEntries(previousSnapshot, drained);
2010
+ const observation = yield* this.checkStable(void 0, previousSnapshot, snapshot, effects);
2011
+ setEntryObservation(entry, observation);
2012
+ }.bind(this)));
2013
+ }
2014
+ finish() {
2015
+ this.assertStarted();
2016
+ if (this.finished) return;
2017
+ for (const state of this.temporal) {
2018
+ if (state.satisfied) continue;
2019
+ const definition = state.definition;
2020
+ if (definition.type === "sometimes") {
2021
+ recordPropertyTemporal(this.coverage, definition.id, "inconclusive");
2022
+ continue;
2023
+ }
2024
+ if (definition.type === "respond" && state.pendingSince === void 0) {
2025
+ recordPropertyTemporal(this.coverage, definition.id, "satisfied");
2026
+ continue;
2027
+ }
2028
+ if (definition.type !== "eventually" && definition.type !== "until" && definition.type !== "respond") {
2029
+ recordPropertyTemporal(this.coverage, definition.id, "satisfied");
2030
+ continue;
2031
+ }
2032
+ if (definition.within !== void 0) {
2033
+ recordPropertyTemporal(this.coverage, definition.id, "inconclusive");
2034
+ this.inconclusiveTemporalIds.push(definition.id);
2035
+ continue;
2036
+ }
2037
+ this.failTemporal(definition);
2038
+ }
2039
+ this.finished = true;
2040
+ }
2041
+ /** Bounded temporal properties that the run ended before deciding. */
2042
+ getInconclusiveTemporalIds() {
2043
+ return this.inconclusiveTemporalIds.slice();
2044
+ }
2045
+ /** The number of stable steps observed so far. */
2046
+ getStableStep() {
2047
+ return this.stableStep;
2048
+ }
2049
+ dispose() {
2050
+ return Effect.runPromise(Effect.gen(function* () {
2051
+ const errors = [];
2052
+ const execution = this.execution;
2053
+ if (execution !== void 0) {
2054
+ this.executionOutcomes = execution.outcomes.slice();
2055
+ const stopExit = yield* Effect.exit(Effect.sync(() => execution.stop()));
2056
+ if (Exit.isFailure(stopExit)) errors.push(Cause.squash(stopExit.cause));
2057
+ this.execution = void 0;
2058
+ }
2059
+ const disposeContext = {
2060
+ passed: this.finished,
2061
+ ...this.failure !== void 0 ? { failure: this.failure } : {}
2062
+ };
2063
+ for (const dispose of [this.sutSession?.dispose, this.referenceSession?.dispose]) {
2064
+ if (dispose === void 0) continue;
2065
+ const disposeExit = yield* Effect.exit(Effect.promise(() => Promise.resolve(dispose(disposeContext))));
2066
+ if (Exit.isFailure(disposeExit)) errors.push(Cause.squash(disposeExit.cause));
2067
+ }
2068
+ this.sutSession = void 0;
2069
+ this.referenceSession = void 0;
2070
+ if (errors.length !== 0) throw new AggregateError(errors, "Property scenario disposal failed");
2071
+ }.bind(this)));
2072
+ }
2073
+ getSnapshot() {
2074
+ return this.snapshot;
2075
+ }
2076
+ getTrace() {
2077
+ const steps = this.timeline.filter((entry) => entry.kind === "event").map((entry) => ({
2078
+ ...entry,
2079
+ phase: entry.command.phase,
2080
+ event: entry.command.event
2081
+ }));
2082
+ return {
2083
+ start: this.startingSnapshot !== void 0 ? {
2084
+ type: "snapshot",
2085
+ snapshot: this.startingSnapshot
2086
+ } : {
2087
+ type: "input",
2088
+ input: this.input
2089
+ },
2090
+ initialSnapshot: this.initialSnapshot,
2091
+ initialEffects: this.initialEffects,
2092
+ initialTransitionIds: this.initialTransitionIds,
2093
+ timeline: this.timeline.slice(),
2094
+ prefixEvents: steps.filter((step) => step.phase === "prefix").map((step) => step.event),
2095
+ events: steps.filter((step) => step.phase === "generated" && step.command.origin === "generator").map((step) => step.event),
2096
+ commands: this.timeline.filter((entry) => entry.kind === "command").map((entry) => entry.command),
2097
+ steps,
2098
+ finalSnapshot: this.snapshot,
2099
+ finalObservation: this.lastObservation,
2100
+ swarm: this.swarmCaseIds,
2101
+ ...this.execution !== void 0 || this.executionOutcomes !== void 0 ? {
2102
+ mode: "executed",
2103
+ outcomes: this.execution?.outcomes.slice() ?? this.executionOutcomes
2104
+ } : {}
2105
+ };
2106
+ }
2107
+ executeEvent(event, phase, origin, sendToSut, compare = true, caseId) {
2108
+ return Effect.gen(function* () {
2109
+ const previousSnapshot = this.snapshot;
2110
+ const execution = this.execution;
2111
+ const { snapshot, effects, transitionIds, drained } = yield* execution !== void 0 ? sendExecutedStep(execution, this.coverage, event) : Effect.succeed(pureTransitionStep(this.logic, this.coverage, previousSnapshot, event));
2112
+ this.snapshot = snapshot;
2113
+ const referenceSession = this.referenceSession;
2114
+ if (referenceSession !== void 0) yield* Effect.promise(() => Promise.resolve(referenceSession.transition(event)));
2115
+ if (sendToSut) {
2116
+ const parsedCase = caseId !== void 0 && caseId.length > 0 ? parsePropertyEventCaseId(caseId) : void 0;
2117
+ yield* Effect.promise(() => Promise.resolve(this.sutSession?.send(event, {
2118
+ snapshot,
2119
+ ...caseId === void 0 ? {} : { caseId },
2120
+ ...parsedCase !== void 0 ? { case: parsedCase } : {}
2121
+ })));
2122
+ }
2123
+ this.coverage.steps++;
2124
+ if (phase === "prefix") this.coverage.prefixSteps++;
2125
+ else this.coverage.generatedSteps++;
2126
+ this.recordSnapshot(snapshot);
2127
+ const entry = {
2128
+ kind: "event",
2129
+ index: this.timeline.length,
2130
+ command: {
2131
+ type: "event",
2132
+ event,
2133
+ phase,
2134
+ origin,
2135
+ caseId
2136
+ },
2137
+ previousSnapshot,
2138
+ snapshot,
2139
+ effects,
2140
+ transitionIds,
2141
+ activeStateIds: this.getActiveStateIds(snapshot)
2142
+ };
2143
+ this.timeline.push(entry);
2144
+ this.markPendingActors(entry);
2145
+ this.pushActorEntries(previousSnapshot, drained);
2146
+ const observation = yield* this.checkStable(event, previousSnapshot, snapshot, effects, compare);
2147
+ setEntryObservation(entry, observation);
2148
+ }.bind(this));
2149
+ }
2150
+ checkStable(event, previousSnapshot, snapshot, effects, compare = true) {
2151
+ return Effect.gen(function* () {
2152
+ const step = this.stableStep++;
2153
+ const observation = compare ? yield* this.compareObservations() : void 0;
2154
+ this.lastObservation = observation;
2155
+ yield* this.checkStateAssertions(snapshot, step);
2156
+ const check = this.sutSession?.check;
2157
+ if (check !== void 0) {
2158
+ const checkExit = yield* Effect.exit(Effect.promise(() => Promise.resolve(check())));
2159
+ if (Exit.isFailure(checkExit)) this.fail(`SUT check failed after ${step} step${step === 1 ? "" : "s"}`, Cause.squash(checkExit.cause), step);
2160
+ }
2161
+ const invariant = this.invariant;
2162
+ if (invariant !== void 0) {
2163
+ this.coverage.invariantChecks++;
2164
+ const invariantExit = yield* Effect.exit(Effect.promise(() => Promise.resolve(invariant({
2165
+ initialSnapshot: this.initialSnapshot,
2166
+ previousSnapshot,
2167
+ snapshot,
2168
+ event,
2169
+ effects,
2170
+ step,
2171
+ label: this.label,
2172
+ classify: this.classify,
2173
+ target: this.target
2174
+ }))));
2175
+ if (Exit.isFailure(invariantExit)) this.fail(`Property invariant failed after ${step} step${step === 1 ? "" : "s"}`, Cause.squash(invariantExit.cause), step);
2176
+ }
2177
+ if (this.targetFunction !== void 0) this.target(this.targetFunction({
2178
+ initialSnapshot: this.initialSnapshot,
2179
+ previousSnapshot,
2180
+ snapshot,
2181
+ event,
2182
+ effects,
2183
+ step,
2184
+ label: this.label,
2185
+ classify: this.classify,
2186
+ target: this.target
2187
+ }));
2188
+ yield* this.checkTemporal({
2189
+ initialSnapshot: this.initialSnapshot,
2190
+ previousSnapshot,
2191
+ snapshot,
2192
+ event,
2193
+ effects,
2194
+ step,
2195
+ label: this.label,
2196
+ classify: this.classify,
2197
+ target: this.target
2198
+ });
2199
+ return observation;
2200
+ }.bind(this));
2201
+ }
2202
+ /**
2203
+ * Runs the per-state assertions and any `meta.test` hooks that apply to
2204
+ * `snapshot`. Both entry points reach this on every stable step.
2205
+ */
2206
+ checkStateAssertions(snapshot, step) {
2207
+ return Effect.gen(function* () {
2208
+ const states = this.sutSession?.states ?? this.states;
2209
+ const session = this.sutSession;
2210
+ const failed = (cause) => this.fail(`State assertion failed after ${step} step${step === 1 ? "" : "s"}: ${cause instanceof Error ? cause.message : String(cause)}`, cause, step);
2211
+ if (states !== void 0) {
2212
+ const keys = Object.keys(states).filter((stateKey) => stateKey !== "*" && matchesStateKey(snapshot, stateKey));
2213
+ if (keys.length === 0 && "*" in states) keys.push("*");
2214
+ for (const key of keys) {
2215
+ const assertion = states[key];
2216
+ if (assertion === void 0) continue;
2217
+ const assertionExit = yield* Effect.exit(Effect.promise(() => Promise.resolve(assertion(snapshot, session))));
2218
+ if (Exit.isFailure(assertionExit)) failed(Cause.squash(assertionExit.cause));
2219
+ }
2220
+ }
2221
+ const getMeta = snapshot.getMeta;
2222
+ if (typeof getMeta !== "function") return;
2223
+ for (const meta of Object.values(getMeta.call(snapshot))) {
2224
+ const test = meta?.test;
2225
+ if (typeof test !== "function") continue;
2226
+ const hook = test;
2227
+ const testExit = yield* Effect.exit(Effect.promise(() => Promise.resolve(hook(session, snapshot))));
2228
+ if (Exit.isFailure(testExit)) failed(Cause.squash(testExit.cause));
2229
+ }
2230
+ }.bind(this));
2231
+ }
2232
+ checkTemporal(context) {
2233
+ return Effect.gen(function* () {
2234
+ for (const state of this.temporal) {
2235
+ if (state.satisfied) continue;
2236
+ this.coverage.temporalChecks++;
2237
+ const definition = state.definition;
2238
+ if (definition.type === "always") {
2239
+ if (!(yield* temporalPredicate(definition.predicate, context))) this.failTemporal(definition);
2240
+ continue;
2241
+ }
2242
+ if (definition.type === "never") {
2243
+ if (yield* temporalPredicate(definition.predicate, context)) this.failTemporal(definition);
2244
+ continue;
2245
+ }
2246
+ if (definition.type === "sometimes") {
2247
+ if (yield* temporalPredicate(definition.predicate, context)) {
2248
+ state.satisfied = true;
2249
+ recordPropertyTemporal(this.coverage, definition.id, "satisfied");
2250
+ }
2251
+ continue;
2252
+ }
2253
+ if (definition.type === "respond") {
2254
+ const respond = yield* Effect.cached(temporalPredicate(definition.response, context));
2255
+ if (state.pendingSince !== void 0) {
2256
+ if (yield* respond) state.pendingSince = void 0;
2257
+ } else if ((yield* temporalPredicate(definition.trigger, context)) && !(yield* respond)) state.pendingSince = context.step;
2258
+ if (state.pendingSince !== void 0 && definition.within !== void 0 && context.step - state.pendingSince >= definition.within) this.failTemporal(definition);
2259
+ continue;
2260
+ }
2261
+ if (definition.type === "eventually") state.satisfied = yield* temporalPredicate(definition.predicate, context);
2262
+ else if (yield* temporalPredicate(definition.until, context)) state.satisfied = true;
2263
+ else if (!(yield* temporalPredicate(definition.hold, context))) this.failTemporal(definition);
2264
+ if (state.satisfied) recordPropertyTemporal(this.coverage, definition.id, "satisfied");
2265
+ if (!state.satisfied && definition.within !== void 0 && context.step >= definition.within) this.failTemporal(definition);
2266
+ }
2267
+ }.bind(this));
2268
+ }
2269
+ failTemporal(definition) {
2270
+ recordPropertyTemporal(this.coverage, definition.id, "failed");
2271
+ const failure = {
2272
+ type: definition.type,
2273
+ id: definition.id,
2274
+ description: definition.description,
2275
+ within: definition.type === "eventually" || definition.type === "until" || definition.type === "respond" ? definition.within : void 0,
2276
+ atStep: this.stableStep - 1
2277
+ };
2278
+ this.fail(`Temporal property "${definition.id}" failed`, failure, failure.atStep, failure);
2279
+ }
2280
+ compareObservations() {
2281
+ return Effect.gen(function* () {
2282
+ const comparableModel = this.sut?.projectModel;
2283
+ const comparableSut = comparableModel !== void 0 && this.sutSession?.read !== void 0 ? this.sut : void 0;
2284
+ const referenceOracle = this.reference;
2285
+ if (referenceOracle === void 0 && comparableSut === void 0) {
2286
+ yield* Effect.promise(() => Promise.resolve(this.sutSession?.settle?.()));
2287
+ return;
2288
+ }
2289
+ yield* Effect.promise(() => Promise.resolve(this.sutSession?.settle?.()));
2290
+ const referenceRaw = yield* Effect.promise(() => Promise.resolve(this.referenceSession?.read()));
2291
+ const sutRaw = yield* Effect.promise(() => Promise.resolve(this.sutSession?.read?.()));
2292
+ const model = referenceOracle !== void 0 ? referenceOracle.projectModel(this.snapshot) : comparableModel?.(this.snapshot);
2293
+ const reference = referenceOracle !== void 0 ? referenceOracle.projectReference !== void 0 ? referenceOracle.projectReference(referenceRaw) : referenceRaw : void 0;
2294
+ const sut = comparableSut !== void 0 ? comparableSut.projectSut !== void 0 ? comparableSut.projectSut(sutRaw) : sutRaw : void 0;
2295
+ const sutModel = comparableSut !== void 0 ? comparableModel?.(this.snapshot) : void 0;
2296
+ const observation = {
2297
+ model,
2298
+ reference: referenceOracle !== void 0 ? {
2299
+ model,
2300
+ observed: reference
2301
+ } : void 0,
2302
+ sut: comparableSut !== void 0 ? {
2303
+ model: sutModel,
2304
+ observed: sut
2305
+ } : void 0
2306
+ };
2307
+ if (referenceOracle !== void 0) this.coverage.oracleComparisons++;
2308
+ if (comparableSut !== void 0) this.coverage.sutComparisons++;
2309
+ const referenceMatches = referenceOracle === void 0 ? true : yield* oracleMatches(referenceOracle.equivalent, model, reference);
2310
+ const sutMatches = comparableSut === void 0 ? true : yield* oracleMatches(comparableSut.equivalent, model, sut);
2311
+ if (!referenceMatches || !sutMatches) {
2312
+ this.lastObservation = observation;
2313
+ this.replaceLastObservation(observation);
2314
+ this.fail("Property observation diverged", {
2315
+ model,
2316
+ reference: observation.reference,
2317
+ sut: observation.sut,
2318
+ referenceMatches,
2319
+ sutMatches
2320
+ }, this.stableStep);
2321
+ }
2322
+ return observation;
2323
+ }.bind(this));
2324
+ }
2325
+ replaceLastObservation(observation) {
2326
+ const last = this.timeline.at(-1);
2327
+ if (last !== void 0) setEntryObservation(last, observation);
2328
+ }
2329
+ /**
2330
+ * Records, on an executed step's timeline entry, the child actors whose
2331
+ * asynchronous work was still in flight when the step settled.
2332
+ */
2333
+ markPendingActors(entry) {
2334
+ if (this.execution === void 0) return;
2335
+ const pending = this.execution.getPendingActors();
2336
+ if (pending.length === 0) return;
2337
+ entry.pendingActors = pending;
2338
+ this.coverage.pendingActorSteps++;
2339
+ }
2340
+ /**
2341
+ * Executed mode only: the milliseconds until the tested actor's pending
2342
+ * `xstate.after` timer for `event` is due, or `undefined` when none is
2343
+ * scheduled.
2344
+ */
2345
+ getAfterTimerRemaining(event) {
2346
+ return this.execution?.getAfterTimerRemaining(event);
2347
+ }
2348
+ /**
2349
+ * Fails the run with `summary`, recording a trace and a replay fixture the
2350
+ * same way an oracle failure does. Used by drivers that detect the run
2351
+ * departed from the sequence they planned.
2352
+ */
2353
+ diverge(summary, cause) {
2354
+ this.assertStarted();
2355
+ this.fail(summary, cause, Math.max(0, this.stableStep - 1));
2356
+ }
2357
+ /**
2358
+ * Appends one timeline entry per transition the actor system performed on
2359
+ * its own during an executed step: invoked/spawned actor lifecycle events,
2360
+ * delayed transitions, and relayed sends.
2361
+ */
2362
+ pushActorEntries(previousSnapshot, drained) {
2363
+ let previous = previousSnapshot;
2364
+ for (const transition of drained) {
2365
+ this.timeline.push({
2366
+ kind: "actorEvent",
2367
+ index: this.timeline.length,
2368
+ source: transition.source,
2369
+ actorId: transition.actorId,
2370
+ event: transition.event,
2371
+ previousSnapshot: previous,
2372
+ snapshot: transition.snapshot,
2373
+ effects: transition.effects,
2374
+ transitionIds: transition.transitionIds
2375
+ });
2376
+ if (transition.source === "root") previous = transition.snapshot;
2377
+ }
2378
+ }
2379
+ recordSnapshot(snapshot) {
2380
+ recordPropertySnapshot(this.coverage, snapshot);
2381
+ }
2382
+ recordGeneratedCommand() {
2383
+ this.generatedCommandCount++;
2384
+ this.coverage.maximumObservedSequenceLength = Math.max(this.coverage.maximumObservedSequenceLength, this.generatedCommandCount);
2385
+ }
2386
+ getActiveStateIds(snapshot) {
2387
+ return (snapshot.nodes ?? snapshot._nodes)?.map((node) => node.id) ?? [];
2388
+ }
2389
+ assertStarted() {
2390
+ if (!this.started) throw new Error("Property scenario runner has not been started");
2391
+ }
2392
+ fail(message, cause, failedAt, temporalFailure) {
2393
+ let fixture;
2394
+ try {
2395
+ fixture = this.getReplayFixture(failedAt, temporalFailure);
2396
+ } catch {
2397
+ fixture = void 0;
2398
+ }
2399
+ this.failure = new ModelTestFailure(message, this.getTrace(), cause, void 0, fixture, void 0, this.formatOptions);
2400
+ throw this.failure;
2401
+ }
2402
+ getReplayFixture(failedAt, temporalFailure) {
2403
+ const identity = this.logic;
2404
+ if (this.startingSnapshot !== void 0 && this.serializeStartingSnapshot === void 0) throw new Error("Property tests starting from a snapshot require serializeSnapshot to create replay fixtures");
2405
+ return {
2406
+ formatVersion: 2,
2407
+ machine: identity.id !== void 0 && identity.id.length > 0 || identity.version !== void 0 && identity.version.length > 0 ? {
2408
+ id: identity.id,
2409
+ version: identity.version
2410
+ } : void 0,
2411
+ start: this.startingSnapshot !== void 0 ? {
2412
+ type: "snapshot",
2413
+ snapshot: this.serializeStartingSnapshot(this.startingSnapshot)
2414
+ } : {
2415
+ type: "input",
2416
+ input: this.input
2417
+ },
2418
+ timeline: this.timeline.filter((entry) => entry.kind !== "actorEvent").map((entry) => ({
2419
+ kind: entry.kind,
2420
+ command: toPortableValue(entry.command)
2421
+ })),
2422
+ failedAt,
2423
+ temporalFailure,
2424
+ ...this.swarmCaseIds !== void 0 ? { swarm: this.swarmCaseIds } : {},
2425
+ ...this.execution !== void 0 ? {
2426
+ mode: "executed",
2427
+ outcomes: toPortableValue(this.execution.outcomes.slice()),
2428
+ ...this.executionConfig?.stubbedSources !== void 0 && this.executionConfig.stubbedSources.length !== 0 ? { stubs: [...this.executionConfig.stubbedSources].sort() } : {}
2429
+ } : {}
2430
+ };
2431
+ }
2432
+ };
2433
+ /**
2434
+ * Replaces every `Error` in `value` with `{ xstate$$error: true, name,
2435
+ * message }`, which JSON can carry: `JSON.stringify(new Error('x'))` is `{}`.
2436
+ * Fixtures are portable data, so this runs on everything they record;
2437
+ * `replayTest()` turns the marked objects back into errors.
2438
+ */
2439
+ function toPortableValue(value, seen = /* @__PURE__ */ new Map()) {
2440
+ if (value instanceof Error) return {
2441
+ xstate$$error: true,
2442
+ name: value.name,
2443
+ message: value.message
2444
+ };
2445
+ if (value === null || typeof value !== "object") return value;
2446
+ if (seen.has(value)) return seen.get(value);
2447
+ if (Array.isArray(value)) {
2448
+ const copy = [];
2449
+ seen.set(value, copy);
2450
+ for (const item of value) copy.push(toPortableValue(item, seen));
2451
+ return copy;
2452
+ }
2453
+ const prototype = Object.getPrototypeOf(value);
2454
+ if (prototype !== Object.prototype && prototype !== null) return value;
2455
+ const copy = {};
2456
+ seen.set(value, copy);
2457
+ for (const [key, nested] of Object.entries(value)) copy[key] = toPortableValue(nested, seen);
2458
+ return copy;
2459
+ }
2460
+ /** Reverses {@link toPortableValue}'s error encoding. */
2461
+ function fromPortableValue(value) {
2462
+ if (value === null || typeof value !== "object") return value;
2463
+ if (Array.isArray(value)) return value.map(fromPortableValue);
2464
+ const prototype = Object.getPrototypeOf(value);
2465
+ if (prototype !== Object.prototype && prototype !== null) return value;
2466
+ const record = value;
2467
+ if (record["xstate$$error"] === true && typeof record["message"] === "string") {
2468
+ const error = new Error(record["message"]);
2469
+ if (typeof record["name"] === "string") error.name = record["name"];
2470
+ return error;
2471
+ }
2472
+ return Object.fromEntries(Object.entries(record).map(([key, nested]) => [key, fromPortableValue(nested)]));
2473
+ }
2474
+ const DEFAULT_BATCH_RUNS = 25;
2475
+ const DEFAULT_MAX_RUNS = 100;
2476
+ const DEFAULT_MAX_FRONTIERS = 5;
2477
+ const DEFAULT_FRONTIER_SEARCH_LIMIT = 1e3;
2478
+ function getCoverageRatio(dimension) {
2479
+ const considered = dimension.covered.length + dimension.uncovered.length;
2480
+ return considered !== 0 ? dimension.covered.length / considered : 1;
2481
+ }
2482
+ function getEventCaseRatio(coverage) {
2483
+ const cases = Object.values(coverage.eventCases);
2484
+ if (cases.length === 0) return 1;
2485
+ return cases.filter((counts) => counts.executed > 0).length / cases.length;
2486
+ }
2487
+ /**
2488
+ * Evaluates a {@link TestStopCondition} against aggregated coverage.
2489
+ *
2490
+ * @experimental
2491
+ */
2492
+ function evaluateTestStopCondition(condition, coverage, elapsedMs) {
2493
+ if (typeof condition === "function") return condition(coverage);
2494
+ const clauses = [];
2495
+ for (const key of [
2496
+ "stateNodes",
2497
+ "transitions",
2498
+ "transitionPairs",
2499
+ "requirements"
2500
+ ]) {
2501
+ const threshold = condition[key];
2502
+ if (threshold !== void 0) clauses.push(getCoverageRatio(coverage[key]) >= threshold);
2503
+ }
2504
+ if (condition.eventCases !== void 0) clauses.push(getEventCaseRatio(coverage) >= condition.eventCases);
2505
+ if (condition.runs !== void 0) clauses.push(coverage.exploration.completedRuns >= condition.runs);
2506
+ if (condition.timeMs !== void 0) clauses.push(elapsedMs >= condition.timeMs);
2507
+ if (condition.any !== void 0 && condition.any.length !== 0) clauses.push(condition.any.some((nested) => evaluateTestStopCondition(nested, coverage, elapsedMs)));
2508
+ return clauses.length > 0 && clauses.every(Boolean);
2509
+ }
2510
+ function getSnapshotStateNodeIds(snapshot) {
2511
+ return (snapshot.nodes ?? snapshot._nodes)?.map((node) => node.id) ?? [];
2512
+ }
2513
+ function getTransitionSourceId(id) {
2514
+ try {
2515
+ const parsed = JSON.parse(id);
2516
+ return Array.isArray(parsed) && parsed[0] === "transition" ? String(parsed[1]) : void 0;
2517
+ } catch {
2518
+ return;
2519
+ }
2520
+ }
2521
+ /**
2522
+ * Picks the shortest paths that reach the state nodes owning the most
2523
+ * uncovered transitions, deduplicated by target configuration.
2524
+ */
2525
+ function selectUncoveredFrontiers(paths, coverage, maxFrontiers) {
2526
+ const uncoveredBySource = /* @__PURE__ */ new Map();
2527
+ for (const id of coverage.transitions.uncovered) {
2528
+ const source = getTransitionSourceId(id);
2529
+ if (source === void 0) continue;
2530
+ uncoveredBySource.set(source, (uncoveredBySource.get(source) ?? 0) + 1);
2531
+ }
2532
+ const ranked = [...uncoveredBySource].sort(([leftId, left], [rightId, right]) => right - left || leftId.localeCompare(rightId));
2533
+ const selected = [];
2534
+ const seen = /* @__PURE__ */ new Set();
2535
+ for (const [sourceId] of ranked) {
2536
+ let best;
2537
+ for (const path of paths) {
2538
+ if (path.steps.length === 0 || !getSnapshotStateNodeIds(path.state).includes(sourceId)) continue;
2539
+ if (best === void 0 || path.weight < best.weight) best = path;
2540
+ }
2541
+ if (best === void 0) continue;
2542
+ const key = getPropertyConfigurationId(best.state);
2543
+ if (seen.has(key)) continue;
2544
+ seen.add(key);
2545
+ selected.push(best);
2546
+ if (selected.length >= maxFrontiers) break;
2547
+ }
2548
+ return selected;
2549
+ }
2550
+ function getLabelExpectationFailures(expectations, coverage) {
2551
+ const failures = [];
2552
+ for (const [name, expectation] of Object.entries(expectations)) {
2553
+ const label = coverage.labels[name] ?? {
2554
+ count: 0,
2555
+ values: {},
2556
+ share: 0
2557
+ };
2558
+ if (expectation.min !== void 0 && label.share < expectation.min) failures.push(`${name}: share ${label.share.toFixed(3)} is below ${expectation.min}`);
2559
+ if (expectation.minCount !== void 0 && label.count < expectation.minCount) failures.push(`${name}: count ${label.count} is below ${expectation.minCount}`);
2560
+ }
2561
+ return failures;
2562
+ }
2563
+ function getFrontierId(frontier) {
2564
+ return JSON.stringify([
2565
+ "frontier",
2566
+ getPropertyConfigurationId(frontier.state),
2567
+ frontier.steps.map((step) => step.event).filter((event) => event.type !== XSTATE_INIT)
2568
+ ]);
2569
+ }
2570
+ /** The id the initial, frontier-less scenario records. */
2571
+ const INITIAL_FRONTIER_ID = JSON.stringify(["frontier", "initial"]);
2572
+ /** A stable key for an event prefix, so equivalent prefixes collapse. */
2573
+ function getEventPrefixKey(events) {
2574
+ return JSON.stringify(events);
2575
+ }
2576
+ /** The swarm-subset PRNG: mulberry32 on a seed offset from the campaign's. */
2577
+ function createSwarmRng(seed) {
2578
+ return createSeededRng(seed ^ 2654435769);
2579
+ }
2580
+ function finalizeExploration(coverage, accumulator) {
2581
+ const maximumSequenceLength = accumulator.maximumSequenceLengthUnknown ? null : accumulator.maximumSequenceLength;
2582
+ if (maximumSequenceLength !== null && coverage.maximumObservedSequenceLength >= maximumSequenceLength) accumulator.truncationReasons.add("maximum sequence length reached");
2583
+ return {
2584
+ strategy: accumulator.strategy,
2585
+ mode: accumulator.mode,
2586
+ configuredRuns: accumulator.configuredRunsOverride ?? (accumulator.configuredRunsUnknown ? null : accumulator.configuredRuns),
2587
+ stoppedBecause: accumulator.stoppedBecause,
2588
+ completedRuns: accumulator.completedRuns,
2589
+ attemptedRuns: coverage.runs,
2590
+ shrinkRuns: coverage.shrinkRuns,
2591
+ maximumSequenceLength,
2592
+ maximumObservedSequenceLength: coverage.maximumObservedSequenceLength,
2593
+ frontiers: accumulator.frontiers.slice(),
2594
+ seeds: accumulator.seeds.slice(),
2595
+ swarm: accumulator.swarmUsed ? {
2596
+ runs: accumulator.swarmRuns,
2597
+ averageEnabled: accumulator.swarmRuns !== 0 ? accumulator.swarmEnabledTotal / accumulator.swarmRuns : 0
2598
+ } : null,
2599
+ target: {
2600
+ best: accumulator.targetBest,
2601
+ label: accumulator.targetLabel,
2602
+ improvements: accumulator.targetImprovements
2603
+ },
2604
+ truncated: accumulator.truncationReasons.size > 0,
2605
+ truncationReasons: [...accumulator.truncationReasons].sort(),
2606
+ pendingActorSteps: coverage.pendingActorSteps
2607
+ };
2608
+ }
2609
+ /**
2610
+ * Warns about bounded temporal properties whose bound exceeds the longest
2611
+ * sequence the campaign can produce: they can never fail.
2612
+ */
2613
+ function getVacuityWarnings(temporal, exploration) {
2614
+ if (exploration.maximumSequenceLength === null) return [];
2615
+ const longest = exploration.maximumSequenceLength + Math.max(0, ...exploration.frontiers.map((frontier) => frontier.prefixLength));
2616
+ const warnings = [];
2617
+ for (const definition of temporal) if ((definition.type === "eventually" || definition.type === "until" || definition.type === "respond") && definition.within !== void 0 && definition.within > longest) warnings.push(`${definition.type} "${definition.id}" has within ${definition.within}, but the longest sequence is ${longest} steps, so it can never fail`);
2618
+ return warnings;
2619
+ }
2620
+ /** Matches a `reachable` target against a snapshot. */
2621
+ function matchesReachableTarget(snapshot, target) {
2622
+ if (matchesStateKey(snapshot, target)) return true;
2623
+ const hasTag = snapshot.hasTag;
2624
+ return typeof hasTag === "function" && hasTag.call(snapshot, target);
2625
+ }
2626
+ /**
2627
+ * The default {@link TestFailureStore} key: the machine id plus a hash of the
2628
+ * event cases and oracles, so campaigns that test the same machine
2629
+ * differently do not replay each other's fixtures.
2630
+ */
2631
+ function getDefaultFailureKey(logic, caseIds, temporal, options) {
2632
+ const signature = JSON.stringify({
2633
+ events: [...caseIds].sort(),
2634
+ temporal: temporal.map(({ type, id }) => `${type}:${id}`).sort(),
2635
+ invariant: options.invariant !== void 0,
2636
+ sut: options.sut !== void 0,
2637
+ reference: options.reference !== void 0,
2638
+ states: Object.keys(options.states ?? {}).sort(),
2639
+ mode: options.mode ?? "pure"
2640
+ });
2641
+ const id = logic.id;
2642
+ return `${typeof id === "string" && id.length > 0 ? id : "machine"}-${fnv1a(signature).toString(16).padStart(8, "0")}`;
2643
+ }
2644
+ /**
2645
+ * The value a thunk produces, or `undefined` when it throws, for the
2646
+ * synchronous projections that must not mask a failure.
2647
+ */
2648
+ const tryValue = (thunk) => {
2649
+ try {
2650
+ return thunk();
2651
+ } catch {
2652
+ return;
2653
+ }
2654
+ };
2655
+ /** The array a thunk produces, or `[]` when it throws. */
2656
+ const tryArray = (thunk) => {
2657
+ try {
2658
+ return thunk();
2659
+ } catch {
2660
+ return [];
2661
+ }
2662
+ };
2663
+ /** The failure with a save note appended to its extras. */
2664
+ const withFailureNote = (failure, note) => new ModelTestFailure(failure.summary, failure.trace, failure.cause, failure.replay, failure.fixture, failure.coverage, failure.format, {
2665
+ ...failure.extras,
2666
+ notes: [...failure.extras?.notes ?? [], note]
2667
+ });
2668
+ /** Reports a campaign's statistics through the package's console logger. */
2669
+ const logTestStatistics = (coverage) => Effect.runSync(Effect.log(formatTestStatistics(coverage)).pipe(Effect.provide(Logger.layer([consoleLineLogger]))));
2670
+ /**
2671
+ * Disposes a runner, recording the run however its disposal settles, so a
2672
+ * throwing disposal still records the run before it propagates.
2673
+ */
2674
+ const disposeRunnerWithRecord = (dispose, record) => Effect.runPromise(ensuringFinalizerWins(Effect.promise(() => Promise.resolve(dispose())), Effect.sync(record)));
2675
+ /** The campaign behind {@link propertyTest}, as one Effect run per call. */
2676
+ const propertyTestProgram = (source, options) => Effect.gen(function* () {
2677
+ const mode = options.mode ?? "pure";
2678
+ if (mode === "pure" && (options.actors !== void 0 || options.outcomes !== void 0)) throw new Error("Property `actors` and `outcomes` require `mode: 'executed'`");
2679
+ const outcomeRegistry = new PropertyOutcomeRegistry();
2680
+ const providedActors = { ...options.actors };
2681
+ for (const src of Object.keys(options.outcomes ?? {})) providedActors[src] = createOutcomeStub(src, outcomeRegistry);
2682
+ const logic = Object.keys(providedActors).length !== 0 ? provideActors(source, providedActors) : source;
2683
+ const { cases: events, descriptors: eventDescriptors } = normalizeEventDescriptors(options.events ?? {});
2684
+ const commands = [];
2685
+ for (const type of [
2686
+ "advance",
2687
+ "checkpoint",
2688
+ "stop"
2689
+ ]) {
2690
+ const configured = options.commands?.[type];
2691
+ if (configured === void 0) continue;
2692
+ const descriptor = isEventDescriptorObject(configured) ? configured : { generate: configured };
2693
+ commands.push({
2694
+ type,
2695
+ generator: descriptor.generate,
2696
+ weight: assertTestWeight(descriptor.weight, `"${type}" command`)
2697
+ });
2698
+ }
2699
+ for (const [src, configured] of Object.entries(options.outcomes ?? {})) {
2700
+ const descriptor = isEventDescriptorObject(configured) ? configured : { generate: configured };
2701
+ commands.push({
2702
+ type: "outcome",
2703
+ src,
2704
+ generator: descriptor.generate,
2705
+ weight: assertTestWeight(descriptor.weight, `"outcome" command for "${src}"`)
2706
+ });
2707
+ }
2708
+ if (options.start !== void 0 && typeof options.start.serializeSnapshot !== "function") throw new Error("Property tests starting from a snapshot require a `start.serializeSnapshot` function");
2709
+ const temporal = [...options.temporal ?? [], ...(options.reachable ?? []).map((target) => ({
2710
+ type: "sometimes",
2711
+ id: `reachable:${target}`,
2712
+ predicate: ({ snapshot }) => matchesReachableTarget(snapshot, target)
2713
+ }))];
2714
+ const coverage = createTestCoverage(logic);
2715
+ for (const event of events) declarePropertyEventCase(coverage, event.caseId, event.weight);
2716
+ const exploration = {
2717
+ strategy: "property",
2718
+ mode,
2719
+ configuredRuns: 0,
2720
+ configuredRunsOverride: null,
2721
+ stoppedBecause: "budget",
2722
+ configuredRunsUnknown: false,
2723
+ completedRuns: 0,
2724
+ maximumSequenceLength: 0,
2725
+ maximumSequenceLengthUnknown: false,
2726
+ frontiers: [],
2727
+ seeds: [],
2728
+ swarmRuns: 0,
2729
+ swarmEnabledTotal: 0,
2730
+ swarmUsed: options.swarm !== void 0 && options.swarm !== false,
2731
+ targetBest: -Infinity,
2732
+ targetLabel: void 0,
2733
+ targetImprovements: 0,
2734
+ truncationReasons: /* @__PURE__ */ new Set()
2735
+ };
2736
+ const configuredFrontiers = options.frontiers;
2737
+ const autoFrontierOptions = configuredFrontiers === "auto" ? { strategy: "uncovered" } : configuredFrontiers !== void 0 && !Array.isArray(configuredFrontiers) && configuredFrontiers.strategy === "uncovered" ? configuredFrontiers : null;
2738
+ const targetFrontierOptions = configuredFrontiers !== void 0 && !Array.isArray(configuredFrontiers) && configuredFrontiers.strategy === "target" ? configuredFrontiers : null;
2739
+ const frontierOptions = Array.isArray(configuredFrontiers) ? { paths: configuredFrontiers } : configuredFrontiers !== void 0 && autoFrontierOptions === null && targetFrontierOptions === null ? configuredFrontiers : null;
2740
+ const frontierContexts = (frontierOptions?.paths ?? []).map((frontier, index) => ({
2741
+ frontier,
2742
+ index,
2743
+ id: getFrontierId(frontier)
2744
+ }));
2745
+ for (const context of frontierContexts) declarePropertyFrontier(coverage, context.id);
2746
+ const selectedFrontiers = frontierContexts.filter((context) => frontierOptions?.select?.(context) ?? true);
2747
+ const scenarios = frontierOptions !== null ? selectedFrontiers : [void 0];
2748
+ const swarmOptions = options.swarm !== void 0 && options.swarm !== false ? options.swarm === true ? {} : options.swarm : null;
2749
+ const swarmCaseIds = events.map((event) => event.caseId);
2750
+ const swarmMinimum = Math.max(1, Math.min(swarmCaseIds.length, swarmOptions?.minCases ?? Math.ceil(swarmCaseIds.length / 2)));
2751
+ const swarmSeed = swarmOptions?.seed ?? 0;
2752
+ /** The enabled subset for one run. Deterministic in `swarmSeed + runIndex`. */
2753
+ const selectSwarmCases = (runIndex) => {
2754
+ const rng = createSwarmRng(swarmSeed + runIndex * 625341585);
2755
+ const shuffled = swarmCaseIds.slice();
2756
+ for (let index = shuffled.length - 1; index > 0; index--) {
2757
+ const swapWith = Math.floor(rng() * (index + 1));
2758
+ const current = shuffled[index];
2759
+ const other = shuffled[swapWith];
2760
+ if (current === void 0 || other === void 0) continue;
2761
+ shuffled[index] = other;
2762
+ shuffled[swapWith] = current;
2763
+ }
2764
+ const count = swarmMinimum + Math.floor(rng() * (shuffled.length - swarmMinimum + 1));
2765
+ return shuffled.slice(0, count).sort();
2766
+ };
2767
+ const targetCandidates = [];
2768
+ const targetFrontierLimit = targetFrontierOptions?.maxFrontiers ?? DEFAULT_MAX_FRONTIERS;
2769
+ const failureStore = options.failures;
2770
+ const failureKey = failureStore !== void 0 ? failureStore.key ?? getDefaultFailureKey(logic, events.map((event) => event.caseId), temporal, options) : void 0;
2771
+ const complete = (result) => Effect.ignoreCause(Effect.promise(() => Promise.resolve(options.sut?.complete?.(result))));
2772
+ /** Saves the failure's fixture and adds `Saved: <location>` to its message. */
2773
+ const saveFailure = (failure) => Effect.gen(function* () {
2774
+ const fixture = failure.fixture;
2775
+ if (failureStore === void 0 || failureKey === void 0 || fixture === void 0 || fixture.start.type !== "input") return failure;
2776
+ const attempt = yield* Effect.exit(Effect.promise(() => Promise.resolve(failureStore.onFailure(fixture, failureKey, failure))));
2777
+ if (Exit.isFailure(attempt)) {
2778
+ const cause = Cause.squash(attempt.cause);
2779
+ return withFailureNote(failure, `Not saved: ${cause instanceof Error ? cause.message : String(cause)}`);
2780
+ }
2781
+ const location = attempt.value;
2782
+ return location === void 0 ? failure : withFailureNote(failure, `Saved: ${location}`);
2783
+ });
2784
+ const recordRun = (runner, runIndex, enabled, run) => {
2785
+ const passed = runner.isFinished();
2786
+ run?.report({
2787
+ passed,
2788
+ swarm: enabled
2789
+ });
2790
+ const trace = tryValue(() => runner.getTrace());
2791
+ if (trace === void 0) return;
2792
+ for (const observation of runner.getTargetObservations()) {
2793
+ if (observation.value > exploration.targetBest) {
2794
+ exploration.targetBest = observation.value;
2795
+ exploration.targetLabel = observation.label;
2796
+ exploration.targetImprovements++;
2797
+ }
2798
+ if (targetFrontierOptions === null) continue;
2799
+ const prefix = trace.timeline.slice(0, observation.index);
2800
+ const prefixEvents = prefix.filter((entry) => entry.kind === "event").map((entry) => entry.command.event);
2801
+ if (prefixEvents.length === 0) continue;
2802
+ const key = getEventPrefixKey(prefixEvents);
2803
+ if (targetCandidates.some((candidate) => candidate.key === key)) continue;
2804
+ const lastPrefixEntry = prefix[prefix.length - 1];
2805
+ if (prefix.length > 0 && lastPrefixEntry === void 0) throw new TypeError("prefix entry is missing");
2806
+ targetCandidates.push({
2807
+ value: observation.value,
2808
+ key,
2809
+ events: prefixEvents,
2810
+ state: lastPrefixEntry?.snapshot ?? trace.initialSnapshot
2811
+ });
2812
+ }
2813
+ targetCandidates.sort((left, right) => right.value - left.value || left.events.length - right.events.length || (left.key < right.key ? -1 : 1));
2814
+ targetCandidates.length = Math.min(targetCandidates.length, targetFrontierLimit * 4);
2815
+ options.collect?.(trace, {
2816
+ passed,
2817
+ runIndex
2818
+ });
2819
+ };
2820
+ const runScenario = (frontierContext, runBudget, runOffset) => Effect.gen(function* () {
2821
+ const prefixEvents = frontierContext !== void 0 ? frontierContext.frontier.steps.map((step) => step.event).filter((event) => event.type !== XSTATE_INIT) : [];
2822
+ if (runBudget !== void 0 && (!Number.isInteger(runBudget) || runBudget < 1)) throw new Error("runsPerFrontier must return a positive integer");
2823
+ const attemptedRunsBefore = coverage.runs;
2824
+ const result = yield* Effect.promise(() => options.adapter.run({
2825
+ events,
2826
+ commands,
2827
+ runBudget,
2828
+ runOffset,
2829
+ createEvent: (type, payload) => {
2830
+ assertEventPayload(payload, type);
2831
+ return {
2832
+ ...payload,
2833
+ type
2834
+ };
2835
+ },
2836
+ createRunner: (run) => {
2837
+ coverage.runs++;
2838
+ if (run?.shrink === true) coverage.shrinkRuns++;
2839
+ const runsBeforeThisOne = coverage.runs - attemptedRunsBefore - 1;
2840
+ const runIndex = (runOffset ?? 0) + runsBeforeThisOne;
2841
+ const runner = new PropertyScenarioRunner(logic, options.input, options.start?.snapshot, options.start?.serializeSnapshot, prefixEvents, frontierContext?.id, options.sut, options.states, options.reference, options.invariant, temporal, eventDescriptors, coverage, mode === "executed" ? {
2842
+ mode,
2843
+ registry: outcomeRegistry,
2844
+ stubbedSources: Object.keys(options.outcomes ?? {})
2845
+ } : void 0);
2846
+ if (options.target !== void 0) runner.setTargetFunction(options.target);
2847
+ runner.setFormatSnapshot(options.formatSnapshot);
2848
+ if (run?.shrink === true) runner.markShrinkRun();
2849
+ const enabled = swarmOptions !== null ? run?.swarm ?? selectSwarmCases(runIndex) : void 0;
2850
+ if (enabled !== void 0) {
2851
+ runner.setSwarm(enabled);
2852
+ exploration.swarmRuns++;
2853
+ exploration.swarmEnabledTotal += enabled.length;
2854
+ }
2855
+ const dispose = runner.dispose.bind(runner);
2856
+ runner.dispose = () => disposeRunnerWithRecord(dispose, () => {
2857
+ recordRun(runner, runIndex, enabled, run);
2858
+ });
2859
+ return runner;
2860
+ }
2861
+ }));
2862
+ const configuredRuns = result.exploration.configuredRuns;
2863
+ if (configuredRuns === null) exploration.configuredRunsUnknown = true;
2864
+ else exploration.configuredRuns += configuredRuns;
2865
+ exploration.completedRuns += result.runs;
2866
+ const maximumSequenceLength = result.exploration.maximumSequenceLength;
2867
+ if (maximumSequenceLength === null) exploration.maximumSequenceLengthUnknown = true;
2868
+ else exploration.maximumSequenceLength = Math.max(exploration.maximumSequenceLength ?? 0, maximumSequenceLength);
2869
+ const frontierId = frontierContext?.id ?? INITIAL_FRONTIER_ID;
2870
+ exploration.frontiers.push({
2871
+ id: frontierId,
2872
+ prefixLength: prefixEvents.length,
2873
+ runBudget: runBudget ?? null,
2874
+ configuredRuns,
2875
+ completedRuns: result.runs,
2876
+ attemptedRuns: coverage.runs - attemptedRunsBefore
2877
+ });
2878
+ exploration.seeds.push({
2879
+ frontierId,
2880
+ engine: result.exploration.engine,
2881
+ seed: result.exploration.seed,
2882
+ path: result.exploration.path
2883
+ });
2884
+ for (const reason of result.exploration.truncationReasons ?? []) exploration.truncationReasons.add(reason);
2885
+ if (result.exploration.truncated === true && (result.exploration.truncationReasons?.length ?? 0) === 0) exploration.truncationReasons.add("adapter reported truncation");
2886
+ if (result.error !== void 0) {
2887
+ exploration.stoppedBecause = "failure";
2888
+ if (result.error instanceof ModelTestFailure) {
2889
+ const failure = yield* saveFailure(new ModelTestFailure(result.error.summary, result.error.trace, result.error.cause, result.replay, result.error.fixture, snapshotCoverage(), result.error.format, result.report !== void 0 ? {
2890
+ ...result.error.extras,
2891
+ report: result.report
2892
+ } : result.error.extras));
2893
+ yield* complete({
2894
+ passed: false,
2895
+ failure
2896
+ });
2897
+ throw failure;
2898
+ }
2899
+ const error = result.error instanceof Error ? result.error : new Error("Property adapter failed", { cause: result.error });
2900
+ yield* complete({
2901
+ passed: false,
2902
+ failure: error
2903
+ });
2904
+ throw error;
2905
+ }
2906
+ });
2907
+ const snapshotCoverage = () => {
2908
+ const bounds = finalizeExploration(coverage, exploration);
2909
+ coverage.temporal.warnings = getVacuityWarnings(temporal, bounds);
2910
+ return finalizeTestCoverage(coverage, bounds);
2911
+ };
2912
+ if (failureStore !== void 0 && failureKey !== void 0 && failureStore.replay !== false) {
2913
+ const store = failureStore;
2914
+ const key = failureKey;
2915
+ const storedFailures = yield* Effect.promise(() => Promise.resolve(store.load(key)));
2916
+ for (const stored of storedFailures) {
2917
+ const replayExit = yield* Effect.exit(Effect.promise(() => replayTest(logic, stored.fixture, {
2918
+ invariant: options.invariant,
2919
+ temporal,
2920
+ reference: options.reference,
2921
+ sut: options.sut,
2922
+ states: options.states,
2923
+ formatSnapshot: options.formatSnapshot,
2924
+ actors: options.actors
2925
+ })));
2926
+ if (Exit.isFailure(replayExit)) {
2927
+ const error = Cause.squash(replayExit.cause);
2928
+ if (error instanceof ReplayNotReproducedError) {
2929
+ yield* Effect.promise(() => Promise.resolve(store.remove?.(stored, key)));
2930
+ continue;
2931
+ }
2932
+ if (error instanceof ModelTestFailure) {
2933
+ const failure = new ModelTestFailure(`${error.summary} (replayed from ${stored.location ?? "the failure store"})`, error.trace, error.cause, error.replay, error.fixture, error.coverage, error.format, error.extras);
2934
+ yield* complete({
2935
+ passed: false,
2936
+ failure
2937
+ });
2938
+ throw failure;
2939
+ }
2940
+ throw error;
2941
+ }
2942
+ }
2943
+ if (store.replay === "only") {
2944
+ exploration.stoppedBecause = "replay";
2945
+ const replayed = snapshotCoverage();
2946
+ yield* complete({ passed: true });
2947
+ return { coverage: replayed };
2948
+ }
2949
+ }
2950
+ const getStaticRunBudget = (frontierContext) => frontierContext !== void 0 ? typeof frontierOptions?.runsPerFrontier === "function" ? frontierOptions.runsPerFrontier(frontierContext) : frontierOptions?.runsPerFrontier : void 0;
2951
+ if (options.until === void 0 && autoFrontierOptions === null && targetFrontierOptions === null) for (const frontierContext of scenarios) yield* runScenario(frontierContext, getStaticRunBudget(frontierContext), void 0);
2952
+ else {
2953
+ const maxRuns = options.maxRuns ?? DEFAULT_MAX_RUNS;
2954
+ const batchRuns = Math.max(1, Math.min(options.batchRuns ?? DEFAULT_BATCH_RUNS, maxRuns));
2955
+ exploration.configuredRunsOverride = maxRuns;
2956
+ const startedAt = DateTime.toEpochMillis(DateTime.nowUnsafe());
2957
+ const shortestPaths = yield* Ref.make(null);
2958
+ const getShortestPathsOnce = () => Effect.gen(function* () {
2959
+ const cached = yield* Ref.get(shortestPaths);
2960
+ if (cached !== null) return cached;
2961
+ const paths = tryArray(() => getShortestPaths(logic, {
2962
+ input: options.input,
2963
+ limit: autoFrontierOptions?.limit ?? DEFAULT_FRONTIER_SEARCH_LIMIT
2964
+ }));
2965
+ yield* Ref.set(shortestPaths, paths);
2966
+ return paths;
2967
+ });
2968
+ const nextFrontierIndex = yield* Ref.make(0);
2969
+ const getAutoScenarios = (budget) => Effect.gen(function* () {
2970
+ const paths = selectUncoveredFrontiers(yield* getShortestPathsOnce(), snapshotCoverage(), autoFrontierOptions.maxFrontiers ?? DEFAULT_MAX_FRONTIERS);
2971
+ if (paths.length === 0) return [[void 0, budget]];
2972
+ const perFrontier = autoFrontierOptions.runsPerFrontier ?? Math.max(1, Math.floor(budget / paths.length));
2973
+ const batch = [];
2974
+ for (const frontier of paths) {
2975
+ const id = getFrontierId(frontier);
2976
+ declarePropertyFrontier(coverage, id);
2977
+ const index = yield* Ref.getAndUpdate(nextFrontierIndex, (value) => value + 1);
2978
+ batch.push([{
2979
+ frontier,
2980
+ index,
2981
+ id
2982
+ }, perFrontier]);
2983
+ }
2984
+ return batch;
2985
+ });
2986
+ const nextTargetIndex = yield* Ref.make(0);
2987
+ const getTargetScenarios = (budget) => Effect.gen(function* () {
2988
+ const candidates = targetCandidates.slice(0, targetFrontierLimit);
2989
+ if (candidates.length === 0) return [[void 0, budget]];
2990
+ const perFrontier = targetFrontierOptions.runsPerFrontier ?? Math.max(1, Math.floor(budget / candidates.length));
2991
+ const batch = [];
2992
+ for (const candidate of candidates) {
2993
+ const frontier = {
2994
+ state: candidate.state,
2995
+ steps: candidate.events.map((event) => ({
2996
+ state: candidate.state,
2997
+ event
2998
+ })),
2999
+ weight: candidate.events.length
3000
+ };
3001
+ const id = getFrontierId(frontier);
3002
+ declarePropertyFrontier(coverage, id);
3003
+ const index = yield* Ref.getAndUpdate(nextTargetIndex, (value) => value + 1);
3004
+ batch.push([{
3005
+ frontier,
3006
+ index,
3007
+ id
3008
+ }, perFrontier]);
3009
+ }
3010
+ return batch;
3011
+ });
3012
+ while (exploration.completedRuns < maxRuns) {
3013
+ const runsBeforeBatch = exploration.completedRuns;
3014
+ const budget = Math.min(batchRuns, maxRuns - exploration.completedRuns);
3015
+ const batch = autoFrontierOptions !== null ? yield* getAutoScenarios(budget) : targetFrontierOptions !== null ? yield* getTargetScenarios(budget) : scenarios.map((frontierContext) => [frontierContext, Math.min(getStaticRunBudget(frontierContext) ?? budget, budget)]);
3016
+ for (const [frontierContext, scenarioBudget] of batch) {
3017
+ yield* runScenario(frontierContext, scenarioBudget, exploration.completedRuns);
3018
+ if (exploration.completedRuns >= maxRuns) break;
3019
+ }
3020
+ if (exploration.completedRuns === runsBeforeBatch) {
3021
+ exploration.truncationReasons.add("adapter made no progress");
3022
+ break;
3023
+ }
3024
+ if (options.until !== void 0 && evaluateTestStopCondition(options.until, snapshotCoverage(), DateTime.toEpochMillis(DateTime.nowUnsafe()) - startedAt)) {
3025
+ exploration.stoppedBecause = "until";
3026
+ break;
3027
+ }
3028
+ }
3029
+ }
3030
+ const finalCoverage = snapshotCoverage();
3031
+ const campaignFailures = [];
3032
+ const completedRuns = finalCoverage.exploration.completedRuns;
3033
+ const reachableCount = options.reachable?.length ?? 0;
3034
+ for (const [index, definition] of temporal.entries()) {
3035
+ if (definition.type !== "sometimes" || finalCoverage.temporal.satisfied.includes(definition.id)) continue;
3036
+ coverage.temporal.campaignFailed.add(definition.id);
3037
+ const reachableIndex = index - (temporal.length - reachableCount);
3038
+ campaignFailures.push(reachableIndex >= 0 ? `reachable "${options.reachable[reachableIndex]}" was not entered in ${completedRuns} run(s)` : `sometimes "${definition.id}" did not hold in ${completedRuns} run(s)`);
3039
+ }
3040
+ if (campaignFailures.length !== 0) {
3041
+ const error = new TestCampaignError(campaignFailures, snapshotCoverage());
3042
+ yield* complete({
3043
+ passed: false,
3044
+ failure: error
3045
+ });
3046
+ throw error;
3047
+ }
3048
+ if (options.expectLabels !== void 0) {
3049
+ const failures = getLabelExpectationFailures(options.expectLabels, finalCoverage);
3050
+ if (failures.length !== 0) {
3051
+ const error = /* @__PURE__ */ new Error(`Property label expectations were not met:\n${failures.map((failure) => ` - ${failure}`).join("\n")}`);
3052
+ error.name = "PropertyLabelExpectationError";
3053
+ error.coverage = finalCoverage;
3054
+ yield* complete({
3055
+ passed: false,
3056
+ failure: error
3057
+ });
3058
+ throw error;
3059
+ }
3060
+ }
3061
+ if (typeof options.statistics === "function") {
3062
+ const deliverReport = options.statistics;
3063
+ const delivery = yield* Effect.promise(() => Promise.resolve().then(() => deliverReport(formatTestStatistics(finalCoverage))).then(() => ({ failed: false }), (error) => ({
3064
+ failed: true,
3065
+ error
3066
+ })));
3067
+ if (delivery.failed) {
3068
+ yield* complete({
3069
+ passed: false,
3070
+ failure: delivery.error
3071
+ });
3072
+ throw delivery.error;
3073
+ }
3074
+ } else if (options.statistics === true) logTestStatistics(finalCoverage);
3075
+ yield* complete({ passed: true });
3076
+ return { coverage: finalCoverage };
3077
+ });
3078
+ /**
3079
+ * Runs generated event and command sequences against the model, and the
3080
+ * system under test when one is given, checking every oracle on every stable
3081
+ * step. Resolves with the campaign's coverage and throws a
3082
+ * {@link ModelTestFailure} on the first failure. Requires an `adapter`;
3083
+ * `@xstate/test` exports a version with fast-check built in.
3084
+ * @experimental
3085
+ */
3086
+ const propertyTest$1 = dual(2, function propertyTest(source, options) {
3087
+ return Effect.runPromise(propertyTestProgram(source, options));
3088
+ });
3089
+ /**
3090
+ * Replaying an `advance` command in pure mode (no executed actor, no SUT
3091
+ * clock) only records the command: the events the original run's clock
3092
+ * delivered are replayed from the `event` entries that follow it, each marked
3093
+ * `origin: 'clock'`. This asserts the fixture still carries those entries in
3094
+ * order, so a hand-edited or re-serialized fixture cannot silently drop the
3095
+ * delivered events and replay a different timeline.
3096
+ *
3097
+ * Only a prefix check: a run that failed part-way through a delivery batch
3098
+ * legitimately records fewer clock entries than `deliveredEvents`.
3099
+ */
3100
+ function assertReplayFixtureClockEvents(fixture) {
3101
+ const timeline = normalizeFixtureTimeline(fixture);
3102
+ for (let index = 0; index < timeline.length; index++) {
3103
+ const entry = timeline[index];
3104
+ if (entry === void 0) continue;
3105
+ const command = entry.command;
3106
+ if (command.type !== "advance" || command.deliveredEvents === void 0 || command.deliveredEvents.length === 0) continue;
3107
+ for (let offset = 0; offset < command.deliveredEvents.length; offset++) {
3108
+ const next = timeline[index + 1 + offset]?.command;
3109
+ if (next === void 0) break;
3110
+ if (next.type !== "event" || next.origin !== "clock") throw new Error(`Property replay fixture is inconsistent: the \`advance\` command at entry ${index} recorded ${command.deliveredEvents.length} delivered event(s), but entry ${index + 1 + offset} is not a clock-delivered event`);
3111
+ if (!defaultEquivalent(next.event, command.deliveredEvents[offset])) throw new Error(`Property replay fixture is inconsistent: the \`advance\` command at entry ${index} recorded ${JSON.stringify(command.deliveredEvents[offset])} as delivered event ${offset}, but entry ${index + 1 + offset} replays ${JSON.stringify(next.event)}`);
3112
+ }
3113
+ }
3114
+ }
3115
+ function normalizeFixtureTimeline(fixture) {
3116
+ if (fixture.formatVersion === 2) return fixture.timeline;
3117
+ return [...fixture.prefixEvents.map((event) => ({
3118
+ kind: "event",
3119
+ command: {
3120
+ type: "event",
3121
+ event,
3122
+ phase: "prefix",
3123
+ origin: "frontier"
3124
+ }
3125
+ })), ...fixture.events.map((event) => ({
3126
+ kind: "event",
3127
+ command: {
3128
+ type: "event",
3129
+ event,
3130
+ phase: "generated",
3131
+ origin: "generator"
3132
+ }
3133
+ }))];
3134
+ }
3135
+ /**
3136
+ * Thrown by `replayTest()` when a fixture recorded from a failing run
3137
+ * replays without reproducing that failure — the regression is fixed, or the
3138
+ * machine no longer behaves the way the fixture recorded.
3139
+ * @experimental
3140
+ */
3141
+ const ReplayNotReproducedErrorBase = Data.TaggedError("@systemfsoftware/xstate-test/engine/propertyTest/ReplayNotReproducedError");
3142
+ var ReplayNotReproducedError = class extends ReplayNotReproducedErrorBase {
3143
+ step;
3144
+ name = "ReplayNotReproducedError";
3145
+ constructor(step) {
3146
+ super({ message: `Property replay did not reproduce the recorded failure at step ${step}` });
3147
+ this.step = step;
3148
+ }
3149
+ };
3150
+ /** The replay behind {@link replayTest}, as one Effect run per call. */
3151
+ const replayTestProgram = (source, fixture, options) => Effect.gen(function* () {
3152
+ const mode = options.mode ?? (fixture.formatVersion === 2 ? fixture.mode : void 0) ?? "pure";
3153
+ const recordedOutcomes = fixture.formatVersion === 2 ? fromPortableValue(fixture.outcomes ?? []) : [];
3154
+ const outcomeRegistry = new PropertyOutcomeRegistry();
3155
+ const commandedSrcs = new Set(normalizeFixtureTimeline(fixture).flatMap((entry) => entry.command.type === "outcome" ? [entry.command.src] : []));
3156
+ const seededOutcomes = recordedOutcomes.filter((record) => !commandedSrcs.has(record.src));
3157
+ const providedActors = { ...options.actors };
3158
+ const stubbedSources = [.../* @__PURE__ */ new Set([...fixture.formatVersion === 2 ? fixture.stubs ?? [] : [], ...recordedOutcomes.map((record) => record.src)])];
3159
+ if (mode === "executed") for (const src of stubbedSources) providedActors[src] ??= createOutcomeStub(src, outcomeRegistry);
3160
+ const logic = Object.keys(providedActors).length !== 0 ? provideActors(source, providedActors) : source;
3161
+ const identity = logic;
3162
+ if (fixture.machine?.id !== void 0 && fixture.machine.id.length > 0 && fixture.machine.id !== identity.id) throw new Error(`Property replay fixture targets machine "${fixture.machine.id}", received "${identity.id ?? "(anonymous)"}"`);
3163
+ if (fixture.machine?.version !== void 0 && fixture.machine.version.length > 0 && fixture.machine.version !== identity.version) throw new Error(`Property replay fixture targets machine version "${fixture.machine.version}", received "${identity.version ?? "(unversioned)"}"`);
3164
+ const startingSnapshot = fixture.start.type === "snapshot" ? options.restoreSnapshot?.(fixture.start.snapshot) : void 0;
3165
+ if (fixture.start.type === "snapshot" && startingSnapshot === void 0) throw new Error("Property replay fixture contains a snapshot but no restoreSnapshot function was provided");
3166
+ const coverage = createTestCoverage(logic);
3167
+ coverage.runs = 1;
3168
+ const serializedStartingSnapshot = fixture.start.type === "snapshot" ? fixture.start.snapshot : void 0;
3169
+ const runner = new PropertyScenarioRunner(logic, fixture.start.type === "input" ? fixture.start.input : void 0, startingSnapshot, fixture.start.type === "snapshot" ? () => serializedStartingSnapshot : void 0, [], void 0, options.sut, options.states, options.reference, options.invariant, options.temporal ?? [], /* @__PURE__ */ new Map(), coverage, mode === "executed" ? {
3170
+ mode,
3171
+ registry: outcomeRegistry,
3172
+ seededOutcomes,
3173
+ stubbedSources
3174
+ } : void 0);
3175
+ runner.setFormatSnapshot(options.formatSnapshot);
3176
+ const failedAt = fixture.failedAt;
3177
+ const timeline = Effect.gen(function* () {
3178
+ yield* Effect.promise(() => runner.start());
3179
+ assertReplayFixtureClockEvents(fixture);
3180
+ for (const entry of normalizeFixtureTimeline(fixture)) {
3181
+ const command = fromPortableValue(entry.command);
3182
+ yield* Effect.promise(() => runner.replay(command));
3183
+ if (failedAt !== void 0 && runner.getStableStep() > failedAt) break;
3184
+ }
3185
+ runner.finish();
3186
+ if (options.expect === "pass") return runner.getTrace();
3187
+ throw new ReplayNotReproducedError(failedAt ?? runner.getStableStep());
3188
+ });
3189
+ return yield* ensuringFinalizerWins(timeline, Effect.promise(() => runner.dispose()));
3190
+ });
3191
+ /**
3192
+ * Replays a {@link TestFixture} without a generator. Resolves with the
3193
+ * replayed trace; see the `expect` option for how failures are reported.
3194
+ * @experimental
3195
+ */
3196
+ const replayTest = dual(3, function replayTest(source, fixture, options) {
3197
+ return Effect.runPromise(replayTestProgram(source, fixture, options));
3198
+ });
3199
+ function serializeSnapshot$1(snapshot) {
3200
+ return "toJSON" in snapshot && typeof snapshot.toJSON === "function" ? snapshot.toJSON() : snapshot;
3201
+ }
3202
+ /**
3203
+ * Converts a trace to JSON-safe data, calling `toJSON()` on snapshots.
3204
+ *
3205
+ * @experimental
3206
+ */
3207
+ function serializeTestTrace(trace) {
3208
+ return {
3209
+ start: trace.start.type === "snapshot" ? {
3210
+ type: "snapshot",
3211
+ snapshot: serializeSnapshot$1(trace.start.snapshot)
3212
+ } : trace.start,
3213
+ initialSnapshot: serializeSnapshot$1(trace.initialSnapshot),
3214
+ initialTransitionIds: trace.initialTransitionIds,
3215
+ timeline: trace.timeline.map((entry) => ({
3216
+ kind: entry.kind,
3217
+ index: entry.index,
3218
+ previousSnapshot: serializeSnapshot$1(entry.previousSnapshot),
3219
+ snapshot: serializeSnapshot$1(entry.snapshot),
3220
+ effects: entry.effects,
3221
+ transitionIds: entry.transitionIds,
3222
+ ...entry.kind === "actorEvent" ? {
3223
+ source: entry.source,
3224
+ actorId: entry.actorId,
3225
+ event: entry.event
3226
+ } : {
3227
+ command: entry.command,
3228
+ observation: entry.observation,
3229
+ ...entry.kind === "event" ? { activeStateIds: entry.activeStateIds } : {}
3230
+ }
3231
+ })),
3232
+ finalSnapshot: serializeSnapshot$1(trace.finalSnapshot),
3233
+ finalObservation: trace.finalObservation
3234
+ };
3235
+ }
3236
+ /**
3237
+ * The default failure-trace projection: `{ value, context }` for machine
3238
+ * snapshots, plus `status`, `output`, `error`, and `tags` when they carry
3239
+ * information. Other snapshots are printed without their runtime bookkeeping.
3240
+ * @experimental
3241
+ */
3242
+ function defaultFormatSnapshot(snapshot) {
3243
+ const json = serializeSnapshot$1(snapshot);
3244
+ if (json === void 0 || typeof json !== "object") return json;
3245
+ const { value, context, status, output, error, tags, _nextTimerId: _timerId, timers: _timers, children: _children, historyValue: _historyValue, sessionId: _sessionId, ...rest } = json;
3246
+ const hasValue = "value" in json;
3247
+ const projected = hasValue ? {
3248
+ value,
3249
+ context
3250
+ } : {
3251
+ ...rest,
3252
+ ...context === void 0 ? {} : { context }
3253
+ };
3254
+ if (hasValue) {
3255
+ if (status !== void 0 && status !== "active") projected["status"] = status;
3256
+ } else if (status !== void 0) projected["status"] = status;
3257
+ if (output !== void 0) projected["output"] = output;
3258
+ if (error !== void 0) projected["error"] = error;
3259
+ if (Array.isArray(tags) ? tags.length > 0 : tags !== void 0) projected["tags"] = tags;
3260
+ return projected;
3261
+ }
3262
+ function stringifyForTrace(value) {
3263
+ try {
3264
+ const json = JSON.stringify(value, (_key, nested) => nested instanceof Error ? {
3265
+ name: nested.name,
3266
+ message: nested.message
3267
+ } : typeof nested === "bigint" ? `${nested}n` : nested);
3268
+ return json === void 0 ? String(value) : json;
3269
+ } catch {
3270
+ return String(value);
3271
+ }
3272
+ }
3273
+ function formatEventForTrace(event) {
3274
+ const { type, sessionId: _sessionId, ...payload } = event;
3275
+ if (type === "xstate.timer" && typeof payload["id"] === "string") return payload["id"];
3276
+ return Object.keys(payload).length !== 0 ? `${type} ${stringifyForTrace(payload)}` : type;
3277
+ }
3278
+ function getEventOrigin(event, fallback) {
3279
+ if (event.type === "xstate.after" || event.type === "xstate.timer" || event.type.startsWith("xstate.after.")) return "timer";
3280
+ if (event.type === "xstate.done.actor" || event.type === "xstate.error.actor" || event.type.startsWith("xstate.done.actor.") || event.type.startsWith("xstate.error.actor.")) return "outcome";
3281
+ return fallback;
3282
+ }
3283
+ /**
3284
+ * Renders a trace as the human-readable text used in failure messages: one
3285
+ * line per step, `N. <origin> <event> -> <state>`, where `<state>` is the
3286
+ * snapshot projected through `formatSnapshot`. In executed mode, the events
3287
+ * the tested actor then processed on its own follow their step as `↳` lines;
3288
+ * child actors' own transitions are left out.
3289
+ * @experimental
3290
+ */
3291
+ const formatTestTrace = dual((args) => args.length >= 2 || typeof args[0] === "object" && args[0] !== null && "timeline" in args[0], function formatTestTrace(trace, options = {}) {
3292
+ const format = (snapshot) => stringifyForTrace((options.formatSnapshot ?? defaultFormatSnapshot)(snapshot));
3293
+ const lines = [`start ${format(trace.initialSnapshot)}`];
3294
+ const pushObservation = (observation) => {
3295
+ for (const [name, compared] of [["reference", observation?.reference], ["sut", observation?.sut]]) {
3296
+ if (compared === void 0 || defaultEquivalent(compared.model, compared.observed)) continue;
3297
+ lines.push(` ${name} diverged`, ` model: ${stringifyForTrace(compared.model)}`, ` observed: ${stringifyForTrace(compared.observed)}`);
3298
+ }
3299
+ };
3300
+ let step = 0;
3301
+ for (const entry of trace.timeline) {
3302
+ if (entry.kind === "actorEvent") {
3303
+ if (entry.source === "root") lines.push(` ↳ ${getEventOrigin(entry.event, "actor")} ${formatEventForTrace(entry.event)} -> ${format(entry.snapshot)}`);
3304
+ continue;
3305
+ }
3306
+ step++;
3307
+ if (entry.kind === "event") {
3308
+ const { command } = entry;
3309
+ const origin = getEventOrigin(command.event, command.origin === "frontier" ? "prefix" : command.origin);
3310
+ lines.push(`${step}. ${origin} ${formatEventForTrace(command.event)} -> ${format(entry.snapshot)}`);
3311
+ } else {
3312
+ const { command } = entry;
3313
+ switch (command.type) {
3314
+ case "advance":
3315
+ lines.push(`${step}. timer advance ${command.milliseconds}ms -> ${format(entry.snapshot)}`);
3316
+ break;
3317
+ case "outcome":
3318
+ lines.push(`${step}. outcome ${command.src} ${stringifyForTrace(command.outcome)} -> ${format(entry.snapshot)}`);
3319
+ break;
3320
+ case "checkpoint":
3321
+ lines.push(`${step}. checkpoint${command.label === void 0 ? "" : ` ${command.label}`}`);
3322
+ break;
3323
+ default: lines.push(`${step}. stop -> ${format(entry.snapshot)}`);
3324
+ }
3325
+ }
3326
+ if (entry.pendingActors !== void 0 && entry.pendingActors.length !== 0) lines.push(` pending actors: ${entry.pendingActors.join(", ")}`);
3327
+ pushObservation(entry.observation);
3328
+ }
3329
+ return lines.join("\n");
3330
+ });
3331
+ /**
3332
+ * The `stateMatcher` for models built from bare logic: `'*'` matches every
3333
+ * snapshot; on a state machine snapshot, `'#id'` matches when the state node
3334
+ * with that id is active and any other key matches through
3335
+ * `snapshot.matches(key)`. Node ids are compared rather than node objects, so
3336
+ * snapshots of a `machine.provide()`d copy match too.
3337
+ */
3338
+ function matchesStateKey(snapshot, stateKey) {
3339
+ if (stateKey === "*") return true;
3340
+ const machineSnapshot = snapshot;
3341
+ if (stateKey.startsWith("#")) {
3342
+ const id = stateKey.slice(1);
3343
+ return machineSnapshot.nodes?.some((node) => node.id === id) ?? false;
3344
+ }
3345
+ if (typeof machineSnapshot.matches !== "function") return false;
3346
+ try {
3347
+ return machineSnapshot.matches(stateKey);
3348
+ } catch {
3349
+ return false;
3350
+ }
3351
+ }
3352
+ // src/engine/suite.ts
3353
+ /** The transition and state node ids a single trace exercised. */
3354
+ function getTraceElements(trace) {
3355
+ const elements = /* @__PURE__ */ new Set();
3356
+ for (const id of trace.initialTransitionIds) elements.add(`transition:${id}`);
3357
+ for (const entry of trace.timeline) {
3358
+ for (const id of entry.transitionIds) elements.add(`transition:${id}`);
3359
+ if (entry.kind === "event") for (const id of entry.activeStateIds) elements.add(`stateNode:${id}`);
3360
+ }
3361
+ return [...elements].sort();
3362
+ }
3363
+ /** A stable identity for a fixture's start and timeline, used to drop duplicates. */
3364
+ function fixtureKey(fixture) {
3365
+ return JSON.stringify([fixture.start, fixture.timeline]);
3366
+ }
3367
+ function toSuiteFixture(trace, machine, serializeStartingSnapshot) {
3368
+ if (trace.start.type === "snapshot" && serializeStartingSnapshot === void 0) throw new Error("Property suites starting from a snapshot require start.serializeSnapshot");
3369
+ return {
3370
+ formatVersion: 2,
3371
+ machine,
3372
+ start: trace.start.type === "snapshot" ? {
3373
+ type: "snapshot",
3374
+ snapshot: serializeStartingSnapshot(trace.start.snapshot)
3375
+ } : trace.start,
3376
+ timeline: trace.timeline.flatMap((entry) => entry.kind === "event" || entry.kind === "command" ? [{
3377
+ kind: entry.kind,
3378
+ command: entry.command
3379
+ }] : []),
3380
+ ...trace.swarm !== void 0 ? { swarm: trace.swarm } : {},
3381
+ ...trace.mode !== void 0 ? { mode: trace.mode } : {},
3382
+ ...trace.outcomes !== void 0 ? { outcomes: trace.outcomes } : {}
3383
+ };
3384
+ }
3385
+ /** Greedy maximum-coverage selection with deterministic tie-breaking. */
3386
+ function selectFixtures(candidates, maxFixtures) {
3387
+ const covered = /* @__PURE__ */ new Set();
3388
+ const remaining = candidates.slice();
3389
+ const selected = [];
3390
+ const limit = maxFixtures ?? Infinity;
3391
+ while (remaining.length !== 0 && selected.length < limit) {
3392
+ let bestIndex = -1;
3393
+ let bestGain = 0;
3394
+ for (let index = 0; index < remaining.length; index++) {
3395
+ const candidate = remaining[index];
3396
+ if (candidate === void 0) continue;
3397
+ const gain = candidate.elements.filter((element) => !covered.has(element)).length;
3398
+ if (gain > bestGain) {
3399
+ bestGain = gain;
3400
+ bestIndex = index;
3401
+ }
3402
+ }
3403
+ if (bestIndex === -1) break;
3404
+ const [candidate] = remaining.splice(bestIndex, 1);
3405
+ if (candidate === void 0) break;
3406
+ selected.push(candidate);
3407
+ for (const element of candidate.elements) covered.add(element);
3408
+ }
3409
+ return selected.sort((left, right) => left.key < right.key ? -1 : 1).map((candidate) => candidate.fixture);
3410
+ }
3411
+ /**
3412
+ * Runs a `propertyTest()` campaign and exports the traces it explored as an
3413
+ * offline suite.
3414
+ *
3415
+ * Only passing runs are recorded: a campaign that finds a counterexample
3416
+ * throws, as `propertyTest()` does.
3417
+ * @experimental
3418
+ */
3419
+ const generateTestSuite$1 = dual(2, function generateTestSuite(source, options) {
3420
+ return Effect.runPromise(Effect.gen(function* () {
3421
+ const traces = [];
3422
+ const { select, maxFixtures, generatedAt, collect, ...rest } = options;
3423
+ const { coverage } = yield* Effect.promise(() => propertyTest$1(source, {
3424
+ ...rest,
3425
+ collect: (trace, info) => {
3426
+ collect?.(trace, info);
3427
+ if (info.passed) traces.push(trace);
3428
+ }
3429
+ }));
3430
+ const logic = source;
3431
+ const machine = logic.id !== void 0 && logic.id.length > 0 || logic.version !== void 0 && logic.version.length > 0 ? {
3432
+ id: logic.id,
3433
+ version: logic.version
3434
+ } : void 0;
3435
+ const serializeStartingSnapshot = options.start?.serializeSnapshot;
3436
+ const seen = /* @__PURE__ */ new Set();
3437
+ const candidates = [];
3438
+ for (const trace of traces) {
3439
+ const fixture = toSuiteFixture(trace, machine, serializeStartingSnapshot);
3440
+ const key = fixtureKey(fixture);
3441
+ if (seen.has(key)) continue;
3442
+ seen.add(key);
3443
+ candidates.push({
3444
+ fixture,
3445
+ elements: getTraceElements(trace),
3446
+ key,
3447
+ length: fixture.timeline.length
3448
+ });
3449
+ }
3450
+ candidates.sort((left, right) => left.length - right.length || (left.key < right.key ? -1 : 1));
3451
+ const fixtures = (select ?? "minimal") === "all" ? candidates.slice(0, maxFixtures ?? candidates.length).map((candidate) => candidate.fixture) : selectFixtures(candidates, maxFixtures);
3452
+ return {
3453
+ formatVersion: 1,
3454
+ machineId: machine?.id,
3455
+ machineVersion: machine?.version,
3456
+ generatedAt,
3457
+ fixtures,
3458
+ coverage: testCoverageToJSON(coverage)
3459
+ };
3460
+ }));
3461
+ });
3462
+ /**
3463
+ * Replays one suite fixture. Resolves when the fixture still passes and
3464
+ * rejects with the underlying failure when it does not.
3465
+ * @experimental
3466
+ */
3467
+ const replayTestSuiteFixture = dual(3, function replayTestSuiteFixture(source, fixture, options) {
3468
+ return Effect.runPromise(Effect.promise(() => replayTest(source, fixture, {
3469
+ ...options,
3470
+ expect: "pass"
3471
+ })).pipe(Effect.asVoid));
3472
+ });
3473
+ /**
3474
+ * Replays every fixture in a suite. Each fixture is expected to pass.
3475
+ *
3476
+ * @experimental
3477
+ */
3478
+ const replayTestSuite = dual(3, function replayTestSuite(source, suite, options) {
3479
+ return Effect.runPromise(Effect.gen(function* () {
3480
+ const passed = yield* Ref.make(0);
3481
+ const failed = [];
3482
+ for (const [index, fixture] of suite.fixtures.entries()) {
3483
+ const exit = yield* Effect.exit(Effect.promise(() => replayTestSuiteFixture(source, fixture, options)));
3484
+ if (Exit.isFailure(exit)) failed.push({
3485
+ fixture,
3486
+ index,
3487
+ title: formatTestSuiteFixtureTitle(fixture, index),
3488
+ error: Cause.squash(exit.cause)
3489
+ });
3490
+ else yield* Ref.update(passed, (value) => value + 1);
3491
+ }
3492
+ return {
3493
+ passed: yield* Ref.get(passed),
3494
+ failed
3495
+ };
3496
+ }));
3497
+ });
3498
+ /**
3499
+ * A stable, human-readable one-line title for a fixture.
3500
+ *
3501
+ * @experimental
3502
+ */
3503
+ const formatTestSuiteFixtureTitle = dual(2, function formatTestSuiteFixtureTitle(fixture, index) {
3504
+ const steps = fixture.timeline.map((entry) => {
3505
+ const command = entry.command;
3506
+ switch (command.type) {
3507
+ case "event": return command.event.type;
3508
+ case "advance": return `@advance(${command.milliseconds})`;
3509
+ case "checkpoint": return "@checkpoint";
3510
+ case "outcome": return `@outcome(${command.src})`;
3511
+ default: return "@stop";
3512
+ }
3513
+ });
3514
+ return `fixture ${index + 1}: ${steps.join(" -> ") || "(no events)"}`;
3515
+ });
3516
+ /**
3517
+ * Registers one test per suite fixture with a vitest/jest-compatible
3518
+ * `it`/`describe` pair.
3519
+ * @experimental
3520
+ */
3521
+ const describeTestSuite = dual(3, function describeTestSuite(suite, source, options) {
3522
+ const globals = globalThis;
3523
+ const it = options.it ?? globals.it;
3524
+ if (it === void 0) throw new Error("describeTestSuite() requires an `it` function when none is global");
3525
+ const register = () => {
3526
+ suite.fixtures.forEach((fixture, index) => {
3527
+ it(formatTestSuiteFixtureTitle(fixture, index), () => replayTestSuiteFixture(source, fixture, options));
3528
+ });
3529
+ };
3530
+ const describe = options.describe ?? globals.describe;
3531
+ const name = options.name ?? `property suite${suite.machineId !== void 0 && suite.machineId.length > 0 ? ` (${suite.machineId})` : ""}`;
3532
+ if (describe !== void 0) {
3533
+ describe(name, register);
3534
+ return;
3535
+ }
3536
+ register();
3537
+ });
3538
+ /**
3539
+ * Serializes a suite with stable key ordering.
3540
+ *
3541
+ * @experimental
3542
+ */
3543
+ function serializeTestSuite(suite) {
3544
+ return JSON.stringify({
3545
+ formatVersion: suite.formatVersion,
3546
+ machineId: suite.machineId,
3547
+ machineVersion: suite.machineVersion,
3548
+ generatedAt: suite.generatedAt,
3549
+ fixtures: suite.fixtures,
3550
+ coverage: suite.coverage
3551
+ }, null, 2);
3552
+ }
3553
+ /**
3554
+ * Parses a serialized suite, rejecting unknown format versions.
3555
+ *
3556
+ * @experimental
3557
+ */
3558
+ function parseTestSuite(json) {
3559
+ const parsed = JSON.parse(json);
3560
+ if (parsed === null || typeof parsed !== "object") throw new Error("Property suite JSON must be an object");
3561
+ if (parsed.formatVersion !== 1) throw new Error(`Unsupported property suite format version: ${String(parsed.formatVersion)}`);
3562
+ if (!Array.isArray(parsed.fixtures)) throw new Error("Property suite JSON must contain a `fixtures` array");
3563
+ return parsed;
3564
+ }
3565
+ // src/engine/deduplicatePaths.ts
3566
+ /**
3567
+ * Deduplicates your paths so that A -> B is not executed separately to A -> B
3568
+ * -> C
3569
+ *
3570
+ * Paths are returned longest first (stable for equal lengths). A path is
3571
+ * dropped when its event sequence is a prefix of, or equal to, the event
3572
+ * sequence of a path already kept.
3573
+ */
3574
+ const deduplicatePaths = dual((args) => args.length >= 2 || Array.isArray(args[0]), (paths, serializeEvent = simpleStringify) => {
3575
+ const pathsWithEventSequence = paths.map((path) => ({
3576
+ path,
3577
+ eventSequence: path.steps.map((step) => serializeEvent(step.event))
3578
+ }));
3579
+ pathsWithEventSequence.sort((a, z) => z.path.steps.length - a.path.steps.length);
3580
+ const root = { children: /* @__PURE__ */ new Map() };
3581
+ const kept = [];
3582
+ for (const { path, eventSequence } of pathsWithEventSequence) {
3583
+ let node = root;
3584
+ for (const event of eventSequence) {
3585
+ node = node.children.get(event);
3586
+ if (node === void 0) break;
3587
+ }
3588
+ if (node !== void 0 && kept.length > 0) continue;
3589
+ let insertAt = root;
3590
+ for (const event of eventSequence) {
3591
+ let child = insertAt.children.get(event);
3592
+ if (child === void 0) {
3593
+ child = { children: /* @__PURE__ */ new Map() };
3594
+ insertAt.children.set(event, child);
3595
+ }
3596
+ insertAt = child;
3597
+ }
3598
+ kept.push(path);
3599
+ }
3600
+ return kept;
3601
+ });
3602
+ // src/engine/testPaths.ts
3603
+ const DEFAULT_SAMPLES = 3;
3604
+ /** Default traversal bound; see {@link PathOptions.limit}. */
3605
+ const DEFAULT_TRAVERSAL_LIMIT = 1e4;
3606
+ /** Lifts a possibly-async value into an Effect, preserving the rejection's identity. */
3607
+ const awaited$3 = (value) => Effect.promise(() => Promise.resolve(value));
3608
+ /** Internal event types path generation drives explicitly. */
3609
+ const DONE_ACTOR_EVENT = "xstate.done.actor";
3610
+ const ERROR_ACTOR_EVENT = "xstate.error.actor";
3611
+ const AFTER_EVENT = "xstate.after";
3612
+ const DONE_STATE_EVENT = "xstate.done.state";
3613
+ /**
3614
+ * The internal events traversal offers. The other internal events (state and
3615
+ * invoke timeouts, actor snapshots) are left out: nothing here can drive them
3616
+ * in executed mode, and a path that assumed one would desynchronize.
3617
+ */
3618
+ const TRAVERSED_INTERNAL_EVENTS = /* @__PURE__ */ new Set([
3619
+ DONE_ACTOR_EVENT,
3620
+ ERROR_ACTOR_EVENT,
3621
+ AFTER_EVENT
3622
+ ]);
3623
+ /** Internal events are the machine's own; only user events have declared cases. */
3624
+ function isInternalEventType(type) {
3625
+ return type.startsWith("xstate.");
3626
+ }
3627
+ function assertGeneratedOutcome(value, src) {
3628
+ if (value === void 0 || value === null || typeof value !== "object" || typeof value.ok !== "boolean") throw new Error(`The \`outcomes\` generator for "${src}" produced ${JSON.stringify(value)} instead of an actor outcome. Generate \`{ ok: true, output }\` or \`{ ok: false, error }\`.`);
3629
+ }
3630
+ /**
3631
+ * Samples concrete actor outcomes from `outcomes`, the same way event payloads
3632
+ * are sampled from `events`, so traversal can label each `xstate.done.actor` /
3633
+ * `xstate.error.actor` step with the outcome that produced it.
3634
+ */
3635
+ function sampleTestOutcomes(outcomes, samples, seed) {
3636
+ const sampled = /* @__PURE__ */ new Map();
3637
+ for (const [src, configured] of Object.entries(outcomes ?? {})) {
3638
+ const generator = isEventDescriptorObject(configured) ? configured.generate : configured;
3639
+ if (generator === void 0) continue;
3640
+ const rng = createSeededRng(deriveCaseSeed(seed, `outcome:${src}`));
3641
+ const values = sampleGenerator(generator, rng, samples);
3642
+ for (const value of values) assertGeneratedOutcome(value, src);
3643
+ sampled.set(src, values);
3644
+ }
3645
+ return sampled;
3646
+ }
3647
+ const LEGACY_EVENT_EXECUTOR_MESSAGE = "A pre-2.0 event executor was passed as an event generator. `events` now declares how event *payloads* are generated; the functions that drive the system under test belong in `sut`. Write the `sut` directly.";
3648
+ /**
3649
+ * A `generate` value without a `resolve` must produce the event payload, so a
3650
+ * sample that is not a plain object is a pre-2.0 executor called with the PRNG
3651
+ * rather than a generator.
3652
+ */
3653
+ function assertGeneratedPayload(value, type) {
3654
+ if (value === void 0) return;
3655
+ if (!(typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.then !== "function")) throw new Error(`Event "${type}" generated ${typeof value === "object" ? "a non-payload value" : `a ${typeof value}`} instead of an event payload object. ${LEGACY_EVENT_EXECUTOR_MESSAGE}`);
3656
+ }
3657
+ /**
3658
+ * Whether the options have the pre-2.0 `TestParam` shape: `events` and
3659
+ * `states` both maps of functions, and no `sut`. The unified API allows that
3660
+ * shape too (bare `(rng) => payload` generators plus top-level `states`), so
3661
+ * it only decides how a generator that does not produce a payload is
3662
+ * reported, never whether the options are rejected up front.
3663
+ */
3664
+ function hasTestParamShape(options) {
3665
+ const isFunctionMap = (value) => {
3666
+ if (value === null || typeof value !== "object") return false;
3667
+ const entries = Object.values(value);
3668
+ return entries.length > 0 && entries.every((entry) => typeof entry === "function");
3669
+ };
3670
+ return options.sut === void 0 && isFunctionMap(options.events) && isFunctionMap(options.states);
3671
+ }
3672
+ /**
3673
+ * Expands an event type into the concrete events traversal should offer,
3674
+ * applying each declared case's `generate`, `resolve`, and `when`.
3675
+ */
3676
+ function createEventExpander(cases, samples, seed, caseIds, testParamShaped) {
3677
+ const drawn = /* @__PURE__ */ new Map();
3678
+ const byType = /* @__PURE__ */ new Map();
3679
+ for (const eventCase of cases) {
3680
+ if (eventCase.generator !== void 0) {
3681
+ const rng = createSeededRng(deriveCaseSeed(seed, eventCase.caseId));
3682
+ const legacyCandidate = testParamShaped && typeof eventCase.generator === "function" && eventCase.descriptor.resolve === void 0;
3683
+ let values;
3684
+ try {
3685
+ values = sampleGenerator(eventCase.generator, rng, samples);
3686
+ } catch (cause) {
3687
+ if (legacyCandidate) throw new Error(`Event "${eventCase.type}" threw when called as a payload generator. ${LEGACY_EVENT_EXECUTOR_MESSAGE}`, { cause });
3688
+ throw cause;
3689
+ }
3690
+ if (eventCase.descriptor.resolve === void 0) for (const value of values) {
3691
+ if (legacyCandidate && typeof value?.then === "function") value.then(void 0, () => {});
3692
+ if (legacyCandidate && value === void 0) throw new Error(`Event "${eventCase.type}" generated \`undefined\` instead of an event payload object. ${LEGACY_EVENT_EXECUTOR_MESSAGE}`);
3693
+ assertGeneratedPayload(value, eventCase.type);
3694
+ }
3695
+ drawn.set(eventCase.caseId, values);
3696
+ }
3697
+ const list = byType.get(eventCase.type) ?? [];
3698
+ list.push(eventCase);
3699
+ byType.set(eventCase.type, list);
3700
+ }
3701
+ return {
3702
+ /** The event types `events` declares, whether or not the machine owns them. */
3703
+ declaredTypes: [...byType.keys()],
3704
+ expand(snapshot, template) {
3705
+ const configured = byType.get(template.type);
3706
+ if (configured === void 0) return [template];
3707
+ const expanded = [];
3708
+ for (const eventCase of configured) {
3709
+ const descriptor = eventCase.descriptor;
3710
+ const generated = drawn.get(eventCase.caseId) ?? [void 0];
3711
+ for (const value of generated) {
3712
+ const payload = descriptor.resolve !== void 0 ? descriptor.resolve({
3713
+ snapshot,
3714
+ generated: value
3715
+ }) : value;
3716
+ if (descriptor.resolve !== void 0 && payload === void 0) continue;
3717
+ const event = {
3718
+ ...template,
3719
+ ...payload ?? {},
3720
+ type: template.type
3721
+ };
3722
+ if (descriptor.when !== void 0 && !descriptor.when({
3723
+ snapshot,
3724
+ event
3725
+ })) continue;
3726
+ caseIds.set(event, eventCase.caseId);
3727
+ expanded.push(event);
3728
+ }
3729
+ }
3730
+ return expanded;
3731
+ }
3732
+ };
3733
+ }
3734
+ function formatEventForPath(event) {
3735
+ const { type, ...payload } = event;
3736
+ return Object.keys(payload).length !== 0 ? `${type} ${JSON.stringify(payload)}` : type;
3737
+ }
3738
+ /** A short, human-readable name for a path: its event types, in order. */
3739
+ function describePath(path) {
3740
+ const described = path.description;
3741
+ if (typeof described === "string" && described.length > 0) return described;
3742
+ const events = path.steps.map((step) => step.event.type).filter((type) => type !== XSTATE_INIT);
3743
+ return events.length !== 0 ? events.join(" → ") : "initial state";
3744
+ }
3745
+ /**
3746
+ * An adapter that executes a fixed list of paths instead of generating
3747
+ * command sequences. One path is one run; there is no shrinking.
3748
+ *
3749
+ * After every step the run's snapshot is compared with the snapshot the
3750
+ * traversal planned for that step, so a run that departs from its path fails
3751
+ * instead of passing on a different path.
3752
+ */
3753
+ function createPathAdapter(paths, caseIds, results, plan) {
3754
+ return { run(request) {
3755
+ return Effect.runPromise(Effect.gen(function* () {
3756
+ const runs = yield* Ref.make(0);
3757
+ const error = yield* Ref.make(void 0);
3758
+ const maximumSequenceLength = yield* Ref.make(0);
3759
+ for (const path of paths) {
3760
+ const steps = path.steps.filter((step) => step.event.type !== XSTATE_INIT);
3761
+ yield* Ref.update(maximumSequenceLength, (value) => Math.max(value, steps.length));
3762
+ const runner = request.createRunner();
3763
+ const pathError = yield* Ref.make(void 0);
3764
+ const runPath = Effect.gen(function* () {
3765
+ yield* awaited$3(runner.start());
3766
+ for (const [index, step] of steps.entries()) {
3767
+ const event = step.event;
3768
+ const diverge = (reason) => runner.diverge(`Path diverged at step ${index + 1}: ${reason}`, {
3769
+ expected: step.state,
3770
+ actual: runner.getSnapshot()
3771
+ });
3772
+ const describeStatus = () => `the model is ${plan.format(runner.getSnapshot())}`;
3773
+ if (plan.mode === "executed") {
3774
+ const resolved = plan.outcomeByEvent.get(event);
3775
+ if (resolved !== void 0) {
3776
+ if (!runner.canRunOutcome()) diverge(`${formatEventForPath(event)} could not be resolved: ${describeStatus()}`);
3777
+ yield* awaited$3(runner.outcome(resolved.src, resolved.outcome));
3778
+ } else if (event.type === AFTER_EVENT) yield* awaited$3(runner.advance(runner.getAfterTimerRemaining(event) ?? diverge(`no timer is pending for ${formatEventForPath(event)}: ${describeStatus()}`)));
3779
+ else if (event.type === DONE_STATE_EVENT) continue;
3780
+ if (resolved !== void 0 || event.type === AFTER_EVENT) {
3781
+ if (plan.serialize(runner.getSnapshot()) !== plan.serialize(step.state)) diverge(`expected ${plan.format(step.state)}, got ${plan.format(runner.getSnapshot())}`);
3782
+ continue;
3783
+ }
3784
+ }
3785
+ const caseId = caseIds.get(event) ?? getPropertyEventCaseId(event.type, "default");
3786
+ if (!runner.canRun(event, caseId)) diverge(`${formatEventForPath(event)} could not be sent: ${describeStatus()}`);
3787
+ yield* awaited$3(runner.run(event, caseId));
3788
+ if (plan.serialize(runner.getSnapshot()) !== plan.serialize(step.state)) diverge(`expected ${plan.format(step.state)}, got ${plan.format(runner.getSnapshot())}`);
3789
+ }
3790
+ runner.finish();
3791
+ });
3792
+ const pathExit = yield* Effect.exit(runPath);
3793
+ if (Exit.isFailure(pathExit)) yield* Ref.set(pathError, squash(pathExit.cause));
3794
+ const disposeExit = yield* Effect.exit(awaited$3(runner.dispose()));
3795
+ const bodyError = yield* Ref.get(pathError);
3796
+ if (Exit.isFailure(disposeExit) && (bodyError === void 0 || bodyError === null)) yield* Ref.set(pathError, squash(disposeExit.cause));
3797
+ yield* Ref.update(runs, (value) => value + 1);
3798
+ const failure = yield* Ref.get(pathError);
3799
+ results.push({
3800
+ path,
3801
+ passed: failure === void 0 || failure === null,
3802
+ error: failure
3803
+ });
3804
+ if (failure !== void 0 && failure !== null) {
3805
+ yield* Ref.set(error, failure);
3806
+ break;
3807
+ }
3808
+ }
3809
+ const runCount = yield* Ref.get(runs);
3810
+ const runError = yield* Ref.get(error);
3811
+ const truncated = runError !== void 0 && runCount < paths.length;
3812
+ const maxSequenceLength = yield* Ref.get(maximumSequenceLength);
3813
+ return {
3814
+ runs: runCount,
3815
+ exploration: {
3816
+ configuredRuns: paths.length,
3817
+ maximumSequenceLength: maxSequenceLength,
3818
+ engine: "paths",
3819
+ truncated,
3820
+ truncationReasons: truncated ? ["counterexample found before every path ran"] : []
3821
+ },
3822
+ ...runError === void 0 ? {} : { error: runError }
3823
+ };
3824
+ }));
3825
+ } };
3826
+ }
3827
+ function withPathExploration(coverage, pathCount, pathGenerator) {
3828
+ const truncationReasons = coverage.exploration.truncationReasons.filter((reason) => reason !== "maximum sequence length reached");
3829
+ return {
3830
+ ...coverage,
3831
+ exploration: {
3832
+ ...coverage.exploration,
3833
+ strategy: "paths",
3834
+ pathCount,
3835
+ pathGenerator,
3836
+ stoppedBecause: coverage.exploration.stoppedBecause === "failure" || coverage.exploration.stoppedBecause === "replay" ? coverage.exploration.stoppedBecause : "paths",
3837
+ truncated: truncationReasons.length > 0,
3838
+ truncationReasons
3839
+ }
3840
+ };
3841
+ }
3842
+ const AFTER_TIMER_PREFIX = "xstate.after.";
3843
+ /**
3844
+ * Executes model paths against a system under test.
3845
+ *
3846
+ * `testPaths()` and `propertyTest()` share their `events`, `sut`, `states`,
3847
+ * `invariant`, `temporal`, and `reference` options and produce the same
3848
+ * coverage object and the same failure type. They differ only in how the event
3849
+ * sequences are produced: traversal of the model's state graph here, generated
3850
+ * command sequences there.
3851
+ * @experimental
3852
+ */
3853
+ const testPaths$1 = dual((args) => args.length >= 2 || typeof args[0] === "object" && args[0] !== null && "transition" in args[0], function testPaths(source, options = {}) {
3854
+ return Effect.runPromise(Effect.suspend(() => {
3855
+ if (options["commands"] !== void 0) throw new Error("`commands` is not supported by path generation; use `propertyTest()`. Paths decide their own `advance` and `outcome` commands from the internal events the traversal took.");
3856
+ const samples = options.samples ?? DEFAULT_SAMPLES;
3857
+ if (!Number.isInteger(samples) || samples < 1) throw new Error(`\`samples\` must be an integer of at least 1 (received ${String(options.samples)}).`);
3858
+ const baseLogic = source;
3859
+ const outcomeSources = Object.keys(options.outcomes ?? {});
3860
+ const testLogic = outcomeSources.length !== 0 ? provideActors(baseLogic, Object.fromEntries(outcomeSources.map((src) => [src, createOutcomeStub(src, void 0)]))) : baseLogic;
3861
+ const { cases } = normalizeEventDescriptors(options.events ?? {});
3862
+ const caseIds = /* @__PURE__ */ new WeakMap();
3863
+ const expander = createEventExpander(cases, samples, options.seed ?? 0, caseIds, hasTestParamShape(options));
3864
+ const mode = options.mode ?? "pure";
3865
+ const sampledOutcomes = sampleTestOutcomes(options.outcomes, samples, options.seed ?? 0);
3866
+ /** Invoke `id` to invoke `src`, collected from the state nodes as seen. */
3867
+ const invokeSrcById = /* @__PURE__ */ new Map();
3868
+ const outcomeByEvent = /* @__PURE__ */ new WeakMap();
3869
+ /**
3870
+ * Virtual time. Timers fire in due order, so a state offers only its
3871
+ * earliest-due `after` transition, where "due" counts the time already
3872
+ * spent in enclosing states along the path that reached it. Timer objects
3873
+ * keep their identity across transitions that do not reschedule them.
3874
+ */
3875
+ const virtualNow = /* @__PURE__ */ new WeakMap();
3876
+ const timerScheduledAt = /* @__PURE__ */ new WeakMap();
3877
+ const afterFiresAt = /* @__PURE__ */ new WeakMap();
3878
+ /**
3879
+ * Turns one synthesized internal event into the concrete events traversal
3880
+ * should offer, and remembers what executed mode has to do to reach it.
3881
+ */
3882
+ const expandInternalEvent = (event) => {
3883
+ if (event.type === AFTER_EVENT) return [{ ...event }];
3884
+ const ok = event.type === DONE_ACTOR_EVENT;
3885
+ const src = invokeSrcById.get(event.actorId);
3886
+ const matching = (src === void 0 ? void 0 : sampledOutcomes.get(src))?.filter((outcome) => outcome.ok === ok) ?? [];
3887
+ return (matching.length !== 0 ? matching : [ok ? {
3888
+ ok: true,
3889
+ output: void 0
3890
+ } : {
3891
+ ok: false,
3892
+ error: /* @__PURE__ */ new Error("generated failure")
3893
+ }]).map((outcome) => {
3894
+ const { sessionId: _sessionId, ...rest } = event;
3895
+ const offered = {
3896
+ ...rest,
3897
+ ...outcome.ok ? { output: outcome.output } : { error: outcome.error }
3898
+ };
3899
+ if (src !== void 0) outcomeByEvent.set(offered, {
3900
+ src,
3901
+ outcome
3902
+ });
3903
+ return offered;
3904
+ });
3905
+ };
3906
+ /** Keeps only the earliest-due `after` events, recording their due time. */
3907
+ const selectDueAfterEvents = (snapshot, afterEvents) => {
3908
+ if (afterEvents.length === 0) return [];
3909
+ const now = virtualNow.get(snapshot) ?? 0;
3910
+ const timers = snapshot.timers ?? {};
3911
+ for (const timer of Object.values(timers)) if (timer !== void 0 && !timerScheduledAt.has(timer)) timerScheduledAt.set(timer, now);
3912
+ const due = afterEvents.map((event) => {
3913
+ const { delay, stateId } = event;
3914
+ const timer = timers[`${AFTER_TIMER_PREFIX}${delay}.${stateId}`];
3915
+ const milliseconds = typeof timer?.delay === "number" ? timer.delay : typeof delay === "number" ? delay : Infinity;
3916
+ return (timer !== void 0 ? timerScheduledAt.get(timer) : now) + milliseconds;
3917
+ });
3918
+ const earliest = Math.min(...due);
3919
+ return afterEvents.filter((event, index) => {
3920
+ if (due[index] !== earliest) return false;
3921
+ afterFiresAt.set(event, earliest);
3922
+ return true;
3923
+ });
3924
+ };
3925
+ const traversalEvents = (snapshot) => {
3926
+ const types = new Set(expander.declaredTypes);
3927
+ const internal = [];
3928
+ const afterEvents = [];
3929
+ if (typeof snapshot.nodes === "object") {
3930
+ for (const stateNode of snapshot.nodes) for (const invokeDef of stateNode.invoke ?? []) invokeSrcById.set(invokeDef.id, invokeDef.src);
3931
+ for (const event of getAllOwnEvents(snapshot)) {
3932
+ const { type } = event;
3933
+ if (!isInternalEventType(type)) types.add(type);
3934
+ else if (TRAVERSED_INTERNAL_EVENTS.has(type)) (type === AFTER_EVENT ? afterEvents : internal).push(...expandInternalEvent(event));
3935
+ }
3936
+ }
3937
+ return [
3938
+ ...[...types].map((type) => ({ type })).flatMap((template) => expander.expand(snapshot, template)),
3939
+ ...internal,
3940
+ ...selectDueAfterEvents(snapshot, afterEvents)
3941
+ ];
3942
+ };
3943
+ const limit = options.limit ?? DEFAULT_TRAVERSAL_LIMIT;
3944
+ const identity = options.serializeState ?? (isMachineLogic(testLogic) ? serializeSnapshot : simpleStringify);
3945
+ const traversalOptions = {
3946
+ events: traversalEvents,
3947
+ input: options.input,
3948
+ limit,
3949
+ ...options.toState === void 0 ? {} : { toState: options.toState },
3950
+ ...options.fromState === void 0 ? {} : { fromState: options.fromState },
3951
+ ...options.stopWhen === void 0 ? {} : { stopWhen: options.stopWhen },
3952
+ serializeState: (snapshot, event, previousSnapshot) => {
3953
+ if (!virtualNow.has(snapshot)) {
3954
+ const previousNow = previousSnapshot !== void 0 && previousSnapshot !== null ? virtualNow.get(previousSnapshot) ?? 0 : 0;
3955
+ virtualNow.set(snapshot, event !== void 0 && event !== null ? afterFiresAt.get(event) ?? previousNow : previousNow);
3956
+ }
3957
+ return identity(snapshot, event, previousSnapshot);
3958
+ },
3959
+ ...options.serializeEvent === void 0 ? {} : { serializeEvent: options.serializeEvent }
3960
+ };
3961
+ function generatePaths(generate) {
3962
+ try {
3963
+ return generate();
3964
+ } catch (error) {
3965
+ if (error instanceof Error && error.message === "Traversal limit exceeded") throw new Error(`Path generation exceeded \`limit\` (${limit} traversal steps) before it exhausted the model. A machine whose context grows without bound never runs out of states, because every distinct context is a distinct state. Pass \`serializeState\` to merge equivalent states, prune traversal with \`stopWhen\` or \`toState\`, or raise \`limit\`.`, { cause: error });
3966
+ throw error;
3967
+ }
3968
+ }
3969
+ let pathGeneratorKind = "shortest";
3970
+ let paths;
3971
+ if (options.paths !== void 0) {
3972
+ pathGeneratorKind = "custom";
3973
+ paths = options.paths;
3974
+ } else if (options.fromEvents !== void 0) {
3975
+ pathGeneratorKind = "events";
3976
+ const { events: _traversalEvents, ...fromEventsOptions } = traversalOptions;
3977
+ paths = generatePaths(() => getPathsFromEvents(testLogic, options.fromEvents, fromEventsOptions));
3978
+ } else {
3979
+ let generated;
3980
+ if (typeof options.pathGenerator === "function") {
3981
+ pathGeneratorKind = "custom";
3982
+ const pathGenerator = options.pathGenerator;
3983
+ generated = generatePaths(() => pathGenerator(testLogic, traversalOptions));
3984
+ } else {
3985
+ pathGeneratorKind = options.pathGenerator ?? "shortest";
3986
+ generated = generatePaths(() => pathGeneratorKind === "simple" ? getSimplePaths(testLogic, traversalOptions) : getShortestPaths(testLogic, traversalOptions));
3987
+ }
3988
+ paths = options.allowDuplicatePaths !== void 0 ? generated : deduplicatePaths(generated);
3989
+ }
3990
+ const declaredTypes = new Set(cases.map((eventCase) => eventCase.type));
3991
+ const extraEvents = {};
3992
+ /** Invoke sources a path resolves, so executed mode can stub them. */
3993
+ const stubbedSources = /* @__PURE__ */ new Set();
3994
+ for (const path of paths) for (const step of path.steps) {
3995
+ const { type } = step.event;
3996
+ if (type === "@xstate.init" || declaredTypes.has(type) || caseIds.has(step.event)) continue;
3997
+ if (isInternalEventType(type)) {
3998
+ const resolved = outcomeByEvent.get(step.event);
3999
+ if (resolved !== void 0) stubbedSources.add(resolved.src);
4000
+ if (mode === "executed") continue;
4001
+ }
4002
+ extraEvents[type] = { generate: void 0 };
4003
+ }
4004
+ const formatSnapshot = options.formatSnapshot ?? defaultFormatSnapshot;
4005
+ const results = [];
4006
+ const adapter = createPathAdapter(paths, caseIds, results, {
4007
+ mode,
4008
+ outcomeByEvent,
4009
+ serialize: (snapshot) => identity(snapshot, void 0, void 0),
4010
+ format: (snapshot) => {
4011
+ try {
4012
+ return JSON.stringify(formatSnapshot(snapshot)) ?? String(snapshot);
4013
+ } catch {
4014
+ return String(snapshot);
4015
+ }
4016
+ }
4017
+ });
4018
+ const { paths: _paths, pathGenerator: _pathGenerator, fromEvents: _fromEvents, limit: _limit, toState: _toState, fromState: _fromState, stopWhen: _stopWhen, samples: _samples, seed: _seed, serializeState: _serializeState, serializeEvent: _serializeEvent, allowDuplicatePaths: _allowDuplicatePaths, events: configuredEvents, outcomes: configuredOutcomes, ...shared } = options;
4019
+ const stubs = {};
4020
+ if (mode === "executed") {
4021
+ for (const src of stubbedSources) stubs[src] = { generate: void 0 };
4022
+ Object.assign(stubs, configuredOutcomes ?? {});
4023
+ }
4024
+ const runSource = mode === "pure" && outcomeSources.length !== 0 ? testLogic : source;
4025
+ return Effect.gen(function* () {
4026
+ const attempt = yield* Effect.exit(awaited$3(propertyTest$1(runSource, {
4027
+ ...shared,
4028
+ adapter,
4029
+ ...Object.keys(stubs).length !== 0 ? { outcomes: stubs } : {},
4030
+ events: {
4031
+ ...configuredEvents ?? {},
4032
+ ...extraEvents
4033
+ }
4034
+ })));
4035
+ if (Exit.isFailure(attempt)) {
4036
+ const error = squash(attempt.cause);
4037
+ if (error instanceof ModelTestFailure && error.coverage !== void 0) {
4038
+ const failedIndex = results.length - 1;
4039
+ const failedPath = results[failedIndex]?.path;
4040
+ const detail = error.summary.replace(/^Property /, "");
4041
+ const remapped = new ModelTestFailure(failedPath !== void 0 ? `Path ${failedIndex + 1} (${describePath(failedPath)}) failed: ${detail.charAt(0).toLowerCase() + detail.slice(1)}` : error.summary, error.trace, error.cause, error.replay, error.fixture, withPathExploration(error.coverage, paths.length, pathGeneratorKind), error.format, error.extras);
4042
+ return yield* Effect.die(remapped);
4043
+ }
4044
+ return yield* Effect.failCause(attempt.cause);
4045
+ }
4046
+ const { coverage } = attempt.value;
4047
+ return {
4048
+ coverage: withPathExploration(coverage, paths.length, pathGeneratorKind),
4049
+ results
4050
+ };
4051
+ });
4052
+ }));
4053
+ });
4054
+ /** Whether `logic` is a state machine (duck-typed, as `xstate/graph` does). */
4055
+ function isMachineLogic(logic) {
4056
+ const machine = logic;
4057
+ return typeof machine["getStateNodeById"] === "function" && typeof machine["resolveState"] === "function" && typeof machine["getTransitionData"] === "function";
4058
+ }
4059
+ // src/scheduler.ts
4060
+ let schedulerStorage;
4061
+ let schedulerStorageLoading;
4062
+ function loadSchedulerStorage() {
4063
+ schedulerStorageLoading ??= import("node:async_hooks").then((hooks) => {
4064
+ schedulerStorage = new hooks.AsyncLocalStorage();
4065
+ return schedulerStorage;
4066
+ });
4067
+ return schedulerStorageLoading;
4068
+ }
4069
+ /**
4070
+ * Awaits `value` as an Effect. A value and a thenable are both accepted, and a
4071
+ * rejection stays the same error object when the effect is run.
4072
+ */
4073
+ const awaited$2 = (value) => Effect.promise(() => Promise.resolve(value));
4074
+ /**
4075
+ * The scheduler fast-check generated for the run currently in flight, or
4076
+ * `undefined` when the adapter was not configured with `scheduler`.
4077
+ *
4078
+ * Only defined while a scheduled run is executing, which is exactly when a
4079
+ * system under test is created and driven.
4080
+ *
4081
+ * @experimental
4082
+ */
4083
+ function getCurrentScheduler() {
4084
+ return schedulerStorage?.getStore();
4085
+ }
4086
+ /** @internal */
4087
+ const withCurrentScheduler = dual(2, function withCurrentScheduler(scheduler, run) {
4088
+ return loadSchedulerStorage().then((storage) => storage.run(scheduler, run));
4089
+ });
4090
+ function scheduleMethod(scheduler, method) {
4091
+ if (method === void 0) return;
4092
+ return scheduler.scheduleFunction((...args) => Effect.runPromise(awaited$2(method(...args))));
4093
+ }
4094
+ /**
4095
+ * Wraps a {@link TestSut} so its asynchronous boundaries (`send`, `read`,
4096
+ * `settle`, `advance`) resolve in an order chosen by the run's fast-check
4097
+ * scheduler instead of in plain microtask order.
4098
+ *
4099
+ * The wrapper binds to {@link getCurrentScheduler} when the session is created,
4100
+ * so it is inert unless the adapter was configured with `scheduler`.
4101
+ *
4102
+ * @experimental
4103
+ */
4104
+ function withScheduledSut(sut) {
4105
+ return {
4106
+ ...sut,
4107
+ create: (context) => Effect.runPromise(Effect.gen(function* () {
4108
+ const scheduler = getCurrentScheduler();
4109
+ const session = yield* awaited$2(sut.create(context));
4110
+ if (scheduler === void 0) return session;
4111
+ const send = scheduler.scheduleFunction((event, sendContext) => Promise.resolve(session.send(event, sendContext)));
4112
+ const read = session.read !== void 0 ? scheduler.scheduleFunction(() => Promise.resolve(session.read())) : void 0;
4113
+ const settle = scheduleMethod(scheduler, session.settle?.bind(session));
4114
+ const advance = scheduleMethod(scheduler, session.advance?.bind(session));
4115
+ return {
4116
+ ...session,
4117
+ send: (event, sendContext) => send(event, sendContext),
4118
+ ...read !== void 0 ? { read: () => read() } : {},
4119
+ ...settle !== void 0 ? { settle: () => settle() } : {},
4120
+ ...advance !== void 0 ? { advance: (milliseconds) => advance(milliseconds) } : {}
4121
+ };
4122
+ }))
4123
+ };
4124
+ }
4125
+ /**
4126
+ * Wraps a reference oracle the way {@link withScheduledSut} wraps a system
4127
+ * under test: its `transition` and `read` calls resolve under the run's
4128
+ * scheduler.
4129
+ *
4130
+ * @experimental
4131
+ */
4132
+ function withScheduledReference(reference) {
4133
+ return {
4134
+ ...reference,
4135
+ create: (context) => Effect.runPromise(Effect.gen(function* () {
4136
+ const scheduler = getCurrentScheduler();
4137
+ const session = yield* awaited$2(reference.create(context));
4138
+ if (scheduler === void 0) return session;
4139
+ const step = scheduler.scheduleFunction((event) => Promise.resolve(session.transition(event)));
4140
+ const read = scheduler.scheduleFunction(() => Promise.resolve(session.read()));
4141
+ return {
4142
+ ...session,
4143
+ transition: (event) => step(event),
4144
+ read: () => read()
4145
+ };
4146
+ }))
4147
+ };
4148
+ }
4149
+ // src/adapter.ts
4150
+ /**
4151
+ * Awaits `value` as an Effect. A value and a thenable are both accepted, and a
4152
+ * rejection stays the same error object when the effect is run.
4153
+ */
4154
+ const awaited$1 = (value) => Effect.promise(() => Promise.resolve(value));
4155
+ function createRunSequencer() {
4156
+ let failureSeen = false;
4157
+ let frozenSwarm;
4158
+ return { nextRun: () => ({
4159
+ shrink: failureSeen,
4160
+ swarm: frozenSwarm,
4161
+ report: ({ passed, swarm }) => {
4162
+ if (passed || failureSeen) return;
4163
+ failureSeen = true;
4164
+ frozenSwarm = swarm;
4165
+ }
4166
+ }) };
4167
+ }
4168
+ /**
4169
+ * Runs one fast-check model scenario: start, the generated commands, then
4170
+ * disposal, so a failing command still disposes the runner.
4171
+ */
4172
+ const runScenario = (runner, generated) => Effect.runPromise(ensuringFinalizerWins(Effect.gen(function* () {
4173
+ yield* awaited$1(runner.start());
4174
+ yield* awaited$1(fc.asyncModelRun(() => ({
4175
+ model: runner,
4176
+ real: void 0
4177
+ }), generated));
4178
+ runner.finish();
4179
+ }), Effect.promise(() => runner.dispose())));
4180
+ /**
4181
+ * Runs the generated command sequence against a fresh runner, releasing the
4182
+ * scheduler's tasks with `waitFor` when one is present.
4183
+ */
4184
+ const runCommands = (request, sequencer, generated, scheduler) => Effect.runPromise(Effect.gen(function* () {
4185
+ const runner = request.createRunner(sequencer.nextRun());
4186
+ const scenario = () => runScenario(runner, generated);
4187
+ if (scheduler === void 0) {
4188
+ yield* awaited$1(scenario());
4189
+ return;
4190
+ }
4191
+ const finished = scenario();
4192
+ yield* ensuringFinalizerWins(awaited$1(scheduler.waitFor(finished)), Effect.promise(() => scheduler.waitIdle()));
4193
+ }));
4194
+ function normalizeSchedulerOptions(scheduler) {
4195
+ if (scheduler === void 0 || scheduler === false) return;
4196
+ return scheduler === true ? {} : scheduler;
4197
+ }
4198
+ function summarizeSchedulerReport(scheduler) {
4199
+ const tasks = scheduler.report().map((item) => ({
4200
+ taskId: item.taskId,
4201
+ label: item.label,
4202
+ schedulingType: item.schedulingType,
4203
+ status: item.status
4204
+ }));
4205
+ return {
4206
+ ordering: tasks.filter(({ status }) => status !== "pending").map(({ taskId }) => taskId),
4207
+ tasks
4208
+ };
4209
+ }
4210
+ /**
4211
+ * `fc.oneof` only accepts integer weights, so relative weights are rescaled by
4212
+ * the smallest one before rounding. Ratios are preserved up to rounding, and
4213
+ * every entry keeps a weight of at least 1 so no case becomes ungeneratable.
4214
+ */
4215
+ function toIntegerWeights(weighted) {
4216
+ const smallest = Math.min(...weighted.map(({ weight }) => weight));
4217
+ return weighted.map(({ arbitrary, weight }) => ({
4218
+ arbitrary,
4219
+ weight: Math.min(MAXIMUM_INTEGER_WEIGHT, Math.max(1, Math.round(weight / smallest)))
4220
+ }));
4221
+ }
4222
+ /** Keeps rescaled weights well inside safe-integer arithmetic. */
4223
+ const MAXIMUM_INTEGER_WEIGHT = 1e6;
4224
+ /**
4225
+ * Extracts the `replayPath` that fast-check embeds in a `fc.commands`
4226
+ * counterexample so a failing run can be replayed deterministically.
4227
+ *
4228
+ * fast-check exposes no typed accessor for this: `CommandsArbitrary` builds a
4229
+ * `CommandsIterable` whose `metadataForReplay()` returns the
4230
+ * `replayPath="<path>"` fragment, and whose `toString()` appends that fragment
4231
+ * inside a trailing `/* ... *\/` comment. Neither member is declared in
4232
+ * `fast-check`'s public typings (checked against fast-check 4.9.0), so this
4233
+ * helper is the single place that depends on that shape. It prefers
4234
+ * `metadataForReplay()` and falls back to parsing `toString()`.
4235
+ *
4236
+ * @experimental
4237
+ */
4238
+ function extractReplayPath(counterexample) {
4239
+ if (counterexample === null || counterexample === void 0) return;
4240
+ const candidate = counterexample;
4241
+ if (typeof candidate.metadataForReplay === "function") {
4242
+ const fromMetadata = parseReplayPathMetadata(candidate.metadataForReplay());
4243
+ if (fromMetadata !== void 0) return fromMetadata;
4244
+ }
4245
+ if (typeof candidate.toString === "function") return parseReplayPathMetadata(candidate.toString.call(counterexample));
4246
+ }
4247
+ function parseReplayPathMetadata(metadata) {
4248
+ if (typeof metadata !== "string") return;
4249
+ const match = metadata.match(/replayPath="([^"]*)"/);
4250
+ if (match === null) return;
4251
+ return match[1];
4252
+ }
4253
+ var EventPropertyCommand = class {
4254
+ type;
4255
+ generated;
4256
+ caseId;
4257
+ constructor(type, generated, caseId) {
4258
+ this.type = type;
4259
+ this.generated = generated;
4260
+ this.caseId = caseId;
4261
+ }
4262
+ check(runner) {
4263
+ return runner.canRunGenerated(this.type, this.generated, this.caseId);
4264
+ }
4265
+ run(runner) {
4266
+ return Effect.runPromise(awaited$1(runner.runGenerated(this.type, this.generated, this.caseId)));
4267
+ }
4268
+ toString() {
4269
+ return `${this.type}(${JSON.stringify(this.generated)})`;
4270
+ }
4271
+ };
4272
+ var AdvancePropertyCommand = class {
4273
+ milliseconds;
4274
+ constructor(milliseconds) {
4275
+ this.milliseconds = milliseconds;
4276
+ }
4277
+ check(runner) {
4278
+ return runner.canRunCommand(runner.getSnapshot().status === "active");
4279
+ }
4280
+ run(runner) {
4281
+ return Effect.runPromise(awaited$1(runner.advance(this.milliseconds)));
4282
+ }
4283
+ toString() {
4284
+ return `advance(${this.milliseconds})`;
4285
+ }
4286
+ };
4287
+ var CheckpointPropertyCommand = class {
4288
+ label;
4289
+ constructor(label) {
4290
+ this.label = label;
4291
+ }
4292
+ check(runner) {
4293
+ return runner.canRunCommand(true);
4294
+ }
4295
+ run(runner) {
4296
+ return Effect.runPromise(awaited$1(runner.checkpoint(this.label)));
4297
+ }
4298
+ toString() {
4299
+ return `checkpoint(${JSON.stringify(this.label)})`;
4300
+ }
4301
+ };
4302
+ var StopPropertyCommand = class {
4303
+ check(runner) {
4304
+ return runner.canRunCommand(runner.getSnapshot().status === "active");
4305
+ }
4306
+ run(runner) {
4307
+ return Effect.runPromise(awaited$1(runner.stop()));
4308
+ }
4309
+ toString() {
4310
+ return "stop()";
4311
+ }
4312
+ };
4313
+ var OutcomePropertyCommand = class {
4314
+ src;
4315
+ outcome;
4316
+ constructor(src, outcome) {
4317
+ this.src = src;
4318
+ this.outcome = outcome;
4319
+ }
4320
+ check(runner) {
4321
+ return runner.canRunOutcome();
4322
+ }
4323
+ run(runner) {
4324
+ return Effect.runPromise(awaited$1(runner.outcome(this.src, this.outcome)));
4325
+ }
4326
+ toString() {
4327
+ return `outcome(${JSON.stringify(this.src)}, ${JSON.stringify(this.outcome)})`;
4328
+ }
4329
+ };
4330
+ var FastCheckAdapter = class {
4331
+ options;
4332
+ kind;
4333
+ constructor(options) {
4334
+ this.options = options;
4335
+ }
4336
+ run(request) {
4337
+ const { options } = this;
4338
+ let schedulerReport;
4339
+ return Effect.runPromise(Effect.gen(function* () {
4340
+ const weighted = request.events.map(({ type, caseId, generator, weight }) => ({
4341
+ arbitrary: generator.map((generated) => new EventPropertyCommand(type, generated, caseId)),
4342
+ weight
4343
+ }));
4344
+ for (const command of request.commands) if (command.type === "advance") weighted.push({
4345
+ arbitrary: command.generator.map((milliseconds) => new AdvancePropertyCommand(milliseconds)),
4346
+ weight: command.weight
4347
+ });
4348
+ else if (command.type === "outcome") {
4349
+ const src = command.src;
4350
+ weighted.push({
4351
+ arbitrary: command.generator.map((outcome) => new OutcomePropertyCommand(src, outcome)),
4352
+ weight: command.weight
4353
+ });
4354
+ } else if (command.type === "checkpoint") weighted.push({
4355
+ arbitrary: command.generator.map((value) => new CheckpointPropertyCommand(value.label)),
4356
+ weight: command.weight
4357
+ });
4358
+ else weighted.push({
4359
+ arbitrary: command.generator.map(() => new StopPropertyCommand()),
4360
+ weight: command.weight
4361
+ });
4362
+ if (weighted.length === 0) throw new Error("Property tests require at least one event or command generator. Generators are derived only from runtime schemas in `schemas.events`: event types declared with a type-only `types<...>()` schema, or with no schema, are skipped, so configure them in `events`.");
4363
+ const commands = weighted.every(({ weight }) => weight === 1) ? weighted.map(({ arbitrary }) => arbitrary) : [fc.oneof(...toIntegerWeights(weighted))];
4364
+ const commandConstraints = {
4365
+ ...options.maxCommands === void 0 ? {} : {
4366
+ maxCommands: options.maxCommands,
4367
+ size: "max"
4368
+ },
4369
+ ...options.replayPath === void 0 ? {} : { replayPath: options.replayPath }
4370
+ };
4371
+ const commandSequence = fc.commands(commands, commandConstraints);
4372
+ const schedulerOptions = normalizeSchedulerOptions(options.scheduler);
4373
+ const sequencer = createRunSequencer();
4374
+ const property = schedulerOptions !== void 0 ? fc.asyncProperty(commandSequence, fc.scheduler(schedulerOptions.act !== void 0 ? { act: schedulerOptions.act } : void 0), (generated, scheduler) => withCurrentScheduler(scheduler, () => runCommands(request, sequencer, generated, scheduler).catch((error) => {
4375
+ schedulerReport = summarizeSchedulerReport(scheduler);
4376
+ throw error;
4377
+ }))) : fc.asyncProperty(commandSequence, (generated) => runCommands(request, sequencer, generated, void 0));
4378
+ const { maxCommands: _, replayPath: __, scheduler: ___, ...parameters } = options;
4379
+ if (request.runBudget !== void 0) parameters.numRuns = request.runBudget;
4380
+ if (request.runOffset !== void 0 && request.runOffset !== 0 && parameters.seed !== void 0) parameters.seed += request.runOffset;
4381
+ const result = yield* awaited$1(fc.check(property, parameters));
4382
+ if (options.asyncReporter !== void 0) yield* awaited$1(options.asyncReporter(result));
4383
+ else if (options.reporter !== void 0) options.reporter(result);
4384
+ const report = result.failed && Number(options.verbose ?? 0) >= 1 ? fc.defaultReportMessage(result) : void 0;
4385
+ const configuredRuns = result.runConfiguration.numRuns ?? 100;
4386
+ const truncationReasons = [];
4387
+ if (result.interrupted) truncationReasons.push("adapter run interrupted");
4388
+ if (result.failed && result.numRuns < configuredRuns) truncationReasons.push(result.errorInstance !== void 0 && result.errorInstance !== null ? "counterexample found before configured runs completed" : "precondition skips exhausted before configured runs completed");
4389
+ const exploration = {
4390
+ configuredRuns,
4391
+ maximumSequenceLength: options.maxCommands ?? null,
4392
+ engine: "fast-check",
4393
+ seed: result.seed,
4394
+ path: result.counterexamplePath ?? void 0,
4395
+ truncated: truncationReasons.length > 0,
4396
+ truncationReasons
4397
+ };
4398
+ if (!result.failed) return {
4399
+ runs: result.numRuns,
4400
+ exploration
4401
+ };
4402
+ return {
4403
+ runs: result.numRuns,
4404
+ exploration,
4405
+ ...report !== void 0 ? { report } : {},
4406
+ error: result.errorInstance ?? /* @__PURE__ */ new Error(result.interrupted ? "FastCheck property run was interrupted" : "FastCheck property run exhausted its precondition skips"),
4407
+ replay: {
4408
+ engine: "fast-check",
4409
+ seed: result.seed,
4410
+ path: result.counterexamplePath ?? void 0,
4411
+ replayPath: extractReplayPath(result.counterexample?.[0]),
4412
+ numShrinks: result.numShrinks,
4413
+ ...schedulerReport !== void 0 ? { data: { scheduler: schedulerReport } } : {}
4414
+ }
4415
+ };
4416
+ }));
4417
+ }
4418
+ };
4419
+ /** @experimental */
4420
+ function fastCheckAdapter(options = {}) {
4421
+ return new FastCheckAdapter(options);
4422
+ }
4423
+ // src/failures.ts
4424
+ /**
4425
+ * Awaits `value` as an Effect. A value and a thenable are both accepted, and a
4426
+ * rejection stays the same error object when the effect is run.
4427
+ */
4428
+ const awaited = (value) => Effect.promise(() => Promise.resolve(value));
4429
+ /** Reads a saved failure file. A file of any other shape is left alone. */
4430
+ const parseSavedFailure = (text) => JSON.parse(text);
4431
+ /** The bytes a saved failure file holds. */
4432
+ const serializeSavedFailure = (saved) => JSON.stringify(saved, null, 2);
4433
+ /** The fixture's JSON, hashed to name its file. */
4434
+ const fixtureJson = (fixture) => JSON.stringify(fixture);
4435
+ const DEFAULT_DIR = ".xstate-test";
4436
+ /** Keeps a key usable as a single directory name. */
4437
+ function sanitizeKey(key) {
4438
+ return key.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "default";
4439
+ }
4440
+ /**
4441
+ * A {@link TestFailureStore} that saves each failing fixture as
4442
+ * `<dir>/<key>/<hash>.json`. Node's `fs` is loaded only when a campaign reads
4443
+ * or writes a failure, so importing `@xstate/test` stays free of Node APIs.
4444
+ *
4445
+ * @experimental
4446
+ */
4447
+ function createFailureDatabase(options = {}) {
4448
+ const dir = options.dir ?? DEFAULT_DIR;
4449
+ const folderOf = (key) => Effect.runPromise(Effect.gen(function* () {
4450
+ return (yield* awaited(import("node:path"))).join(dir, sanitizeKey(key));
4451
+ }));
4452
+ return {
4453
+ ...options.key === void 0 ? {} : { key: options.key },
4454
+ replay: options.replay ?? "first",
4455
+ load: (key) => Effect.runPromise(Effect.gen(function* () {
4456
+ const [fs, path] = yield* awaited(Promise.all([import("node:fs/promises"), import("node:path")]));
4457
+ const folder = yield* awaited(folderOf(key));
4458
+ const names = yield* Effect.catchCause(awaited(fs.readdir(folder)), () => Effect.succeed([]));
4459
+ const stored = [];
4460
+ for (const name of names.filter((file) => file.endsWith(".json")).sort()) {
4461
+ const location = path.join(folder, name);
4462
+ yield* Effect.ignoreCause(Effect.gen(function* () {
4463
+ const saved = parseSavedFailure(yield* awaited(fs.readFile(location, "utf8")));
4464
+ if (saved?.fixture !== void 0) stored.push({
4465
+ fixture: saved.fixture,
4466
+ location
4467
+ });
4468
+ }));
4469
+ }
4470
+ return stored;
4471
+ })),
4472
+ onFailure: (fixture, key, failure) => Effect.runPromise(Effect.gen(function* () {
4473
+ const [fs, path, crypto] = yield* awaited(Promise.all([
4474
+ import("node:fs/promises"),
4475
+ import("node:path"),
4476
+ import("node:crypto")
4477
+ ]));
4478
+ const folder = yield* awaited(folderOf(key));
4479
+ const serialized = fixtureJson(fixture);
4480
+ const hash = crypto.createHash("sha256").update(serialized).digest("hex").slice(0, 12);
4481
+ const location = path.join(folder, `${hash}.json`);
4482
+ const saved = {
4483
+ formatVersion: 1,
4484
+ key,
4485
+ summary: failure.summary,
4486
+ ...failure.replay !== void 0 ? { replay: failure.replay } : {},
4487
+ fixture
4488
+ };
4489
+ yield* awaited(fs.mkdir(folder, { recursive: true }));
4490
+ yield* awaited(fs.writeFile(location, `${serializeSavedFailure(saved)}\n`));
4491
+ return location;
4492
+ })),
4493
+ remove: (stored) => Effect.runPromise(Effect.gen(function* () {
4494
+ if (stored.location === void 0 || stored.location.length === 0) return;
4495
+ const fs = yield* awaited(import("node:fs/promises"));
4496
+ yield* awaited(fs.rm(stored.location, { force: true }));
4497
+ }))
4498
+ };
4499
+ }
4500
+ /** Turns the `failures` option into the store `xstate/graph` expects. */
4501
+ function resolveFailuresOption(failures) {
4502
+ if (failures === void 0 || failures === false) return;
4503
+ if (failures === true) return createFailureDatabase();
4504
+ if (typeof failures.load === "function") return failures;
4505
+ return createFailureDatabase(failures);
4506
+ }
4507
+ // src/propertyTest.ts
4508
+ const ADAPTER_OPTION_KEY_SET = /* @__PURE__ */ new Set([
4509
+ "asyncReporter",
4510
+ "endOnFailure",
4511
+ "ignoreEqualValues",
4512
+ "includeErrorInReport",
4513
+ "interruptAfterTimeLimit",
4514
+ "markInterruptAsFailure",
4515
+ "maxCommands",
4516
+ "maxSkipsPerRun",
4517
+ "numRuns",
4518
+ "path",
4519
+ "randomType",
4520
+ "replayPath",
4521
+ "reporter",
4522
+ "scheduler",
4523
+ "seed",
4524
+ "skipAllAfterTimeLimit",
4525
+ "skipEqualValues",
4526
+ "timeout",
4527
+ "unbiased",
4528
+ "verbose"
4529
+ ]);
4530
+ /**
4531
+ * Derives generators for the event types the machine declares a runtime schema
4532
+ * for and that `events` does not already cover.
4533
+ *
4534
+ * Event types configured explicitly, and type-only `types<...>()` declarations,
4535
+ * are filtered out before `eventsFromSchemas()` sees them. Anything left that
4536
+ * the converters do not recognize still throws, so an unsupported schema
4537
+ * library is reported rather than silently ignored.
4538
+ */
4539
+ function deriveMissingEvents(source, events) {
4540
+ const schemas = source?.schemas?.events;
4541
+ if (schemas === void 0) return;
4542
+ const missing = Object.fromEntries(Object.entries(schemas).filter(([type, schema]) => events?.[type] === void 0 && !isTypeOnlySchema(schema)));
4543
+ if (Object.keys(missing).length === 0) return;
4544
+ return eventsFromSchemas({
4545
+ schemas: { events: missing },
4546
+ events: []
4547
+ }, { eventsWithoutSchema: "skip" });
4548
+ }
4549
+ function splitOptions(options) {
4550
+ const adapterOptions = {};
4551
+ const rest = {};
4552
+ for (const [key, value] of Object.entries(options)) if (ADAPTER_OPTION_KEY_SET.has(key)) adapterOptions[key] = value;
4553
+ else rest[key] = value;
4554
+ return {
4555
+ adapterOptions,
4556
+ rest
4557
+ };
4558
+ }
4559
+ /**
4560
+ * Builds the options `xstate/graph` expects: the implicit adapter, and the
4561
+ * schema-derived `events` map that explicit entries are layered onto.
4562
+ */
4563
+ function resolveOptions(source, options) {
4564
+ const { adapterOptions, rest } = splitOptions(options);
4565
+ const { deriveEvents, adapter, events, failures, ...propertyOptions } = rest;
4566
+ const failureStore = resolveFailuresOption(failures);
4567
+ const derived = deriveEvents === false ? void 0 : deriveMissingEvents(source, events);
4568
+ const { maxRuns } = propertyOptions;
4569
+ return {
4570
+ ...propertyOptions,
4571
+ ...maxRuns === void 0 && typeof adapterOptions["numRuns"] === "number" ? { maxRuns: adapterOptions["numRuns"] } : {},
4572
+ events: derived !== void 0 ? {
4573
+ ...derived,
4574
+ ...events
4575
+ } : events ?? {},
4576
+ adapter: adapter ?? fastCheckAdapter(adapterOptions),
4577
+ ...failureStore !== void 0 ? { failures: failureStore } : {}
4578
+ };
4579
+ }
4580
+ /**
4581
+ * Runs a property-testing campaign with fast-check as the generator.
4582
+ *
4583
+ * Identical to `propertyTest()` from `xstate/graph`, except that fast-check
4584
+ * options (`seed`, `numRuns`, `maxCommands`, `scheduler`, …) are top-level
4585
+ * options rather than `fastCheckAdapter()` arguments, and `events` is derived
4586
+ * from the machine's `schemas.events` when it declares them.
4587
+ *
4588
+ * @experimental
4589
+ */
4590
+ const propertyTest = dual(2, function propertyTest(source, options) {
4591
+ return propertyTest$1(source, resolveOptions(source, options));
4592
+ });
4593
+ /**
4594
+ * Records an offline property suite from a passing campaign, with fast-check
4595
+ * as the generator. See `generateTestSuite()` in `xstate/graph`.
4596
+ *
4597
+ * @experimental
4598
+ */
4599
+ const generateTestSuite = dual(2, function generateTestSuite(source, options) {
4600
+ return generateTestSuite$1(source, resolveOptions(source, options));
4601
+ });
4602
+ /** fast-check arbitraries expose a `generate` method; plain generators do not. */
4603
+ function isArbitrary(value) {
4604
+ return value !== void 0 && value !== null && typeof value === "object" && typeof value.generate === "function";
4605
+ }
4606
+ /**
4607
+ * Replaces every fast-check arbitrary with a `{ sample(rng) }` generator
4608
+ * backed by `fc.sample`, so `xstate/graph` can expand it without depending on
4609
+ * fast-check. An object rather than a function, so `testPaths()` never
4610
+ * mistakes it for a pre-2.0 event executor.
4611
+ */
4612
+ function sampleArbitraries(events, samples, seed, prefix = "") {
4613
+ const sampled = (generator, caseId) => {
4614
+ if (!isArbitrary(generator)) return generator;
4615
+ const values = fc.sample(generator, {
4616
+ seed: deriveCaseSeed(seed, caseId),
4617
+ numRuns: Math.max(1, samples)
4618
+ });
4619
+ let index = 0;
4620
+ return { sample: () => values[index++ % values.length] };
4621
+ };
4622
+ const one = (eventCase, type) => {
4623
+ if (!isEventDescriptorObject(eventCase)) return sampled(eventCase, `${prefix}${type}:default`);
4624
+ const descriptor = eventCase;
4625
+ return {
4626
+ ...descriptor,
4627
+ generate: sampled(descriptor.generate, `${prefix}${type}:${descriptor.case ?? "default"}`)
4628
+ };
4629
+ };
4630
+ return Object.fromEntries(Object.entries(events ?? {}).map(([type, configured]) => [type, Array.isArray(configured) ? configured.map((eventCase) => one(eventCase, type)) : one(configured, type)]));
4631
+ }
4632
+ /**
4633
+ * Executes model paths against a system under test.
4634
+ *
4635
+ * The same options as `propertyTest()`, minus the generation controls
4636
+ * (`numRuns`, `maxCommands`, `swarm`, `frontiers`, …) and plus the traversal
4637
+ * controls (`pathGenerator`, `limit`, `toState`, `samples`, `seed`). fast-check
4638
+ * arbitraries are accepted in `events` and sampled into `samples` concrete
4639
+ * payloads before traversal.
4640
+ *
4641
+ * @experimental
4642
+ */
4643
+ const testPaths = dual((args) => args.length >= 2 || typeof args[0] === "object" && args[0] !== null && "transition" in args[0], function testPaths(source, options = {}) {
4644
+ const { deriveEvents, events, outcomes, failures, ...rest } = options;
4645
+ const failureStore = resolveFailuresOption(failures);
4646
+ const derived = deriveEvents === false ? void 0 : deriveMissingEvents(source, events);
4647
+ const merged = derived !== void 0 ? {
4648
+ ...derived,
4649
+ ...events
4650
+ } : events ?? {};
4651
+ const samples = options.samples ?? 3;
4652
+ const seed = options.seed ?? 0;
4653
+ return testPaths$1(source, {
4654
+ ...rest,
4655
+ ...failureStore !== void 0 ? { failures: failureStore } : {},
4656
+ ...outcomes !== void 0 ? { outcomes: sampleArbitraries(outcomes, samples, seed, "outcome:") } : {},
4657
+ events: sampleArbitraries(merged, samples, seed)
4658
+ });
4659
+ });
4660
+ export { consoleLineLogger as C, formatTestStatistics as D, formatTestCoverageJUnit as E, testCoverageToJSON as O, assertTestCoverage as S, formatTestCoverageHTML as T, ReplayNotReproducedError as _, extractReplayPath as a, replayTest as b, withScheduledReference as c, parseTestSuite as d, replayTestSuite as f, PropertyScenarioRunner as g, ModelTestFailure as h, createFailureDatabase as i, createPickDescriptor as k, withScheduledSut as l, serializeTestSuite as m, propertyTest as n, fastCheckAdapter as o, replayTestSuiteFixture as p, testPaths as r, getCurrentScheduler as s, generateTestSuite as t, describeTestSuite as u, TestCampaignError as v, formatTestCoverage as w, serializeTestTrace as x, defaultEquivalent as y };