@typeonce/effect-machine 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +410 -17
  2. package/dist/Machine.d.ts +1191 -510
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +371 -565
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +57 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -0
  8. package/dist/internal/machine/activities.js +49 -0
  9. package/dist/internal/machine/activities.js.map +1 -0
  10. package/dist/internal/machine/atom.d.ts +69 -0
  11. package/dist/internal/machine/atom.d.ts.map +1 -0
  12. package/dist/{AtomMachine.js → internal/machine/atom.js} +20 -95
  13. package/dist/internal/machine/atom.js.map +1 -0
  14. package/dist/internal/machine/cluster.d.ts +63 -0
  15. package/dist/internal/machine/cluster.d.ts.map +1 -0
  16. package/dist/{ClusterMachine.js → internal/machine/cluster.js} +15 -119
  17. package/dist/internal/machine/cluster.js.map +1 -0
  18. package/dist/internal/machine/command.d.ts +16 -0
  19. package/dist/internal/machine/command.d.ts.map +1 -0
  20. package/dist/internal/machine/command.js +45 -0
  21. package/dist/internal/machine/command.js.map +1 -0
  22. package/dist/internal/machine/commandRuntime.d.ts +13 -0
  23. package/dist/internal/machine/commandRuntime.d.ts.map +1 -0
  24. package/dist/internal/machine/commandRuntime.js +16 -0
  25. package/dist/internal/machine/commandRuntime.js.map +1 -0
  26. package/dist/internal/{machineModel.d.ts → machine/configuration.d.ts} +12 -61
  27. package/dist/internal/machine/configuration.d.ts.map +1 -0
  28. package/dist/internal/{machineModel.js → machine/configuration.js} +250 -545
  29. package/dist/internal/machine/configuration.js.map +1 -0
  30. package/dist/internal/{machineErrors.d.ts → machine/errors.d.ts} +8 -8
  31. package/dist/internal/machine/errors.d.ts.map +1 -0
  32. package/dist/internal/{machineErrors.js → machine/errors.js} +8 -8
  33. package/dist/internal/machine/errors.js.map +1 -0
  34. package/dist/internal/machine/executionPlan.d.ts +49 -0
  35. package/dist/internal/machine/executionPlan.d.ts.map +1 -0
  36. package/dist/internal/machine/executionPlan.js +676 -0
  37. package/dist/internal/machine/executionPlan.js.map +1 -0
  38. package/dist/internal/machine/invocation.d.ts +23 -0
  39. package/dist/internal/machine/invocation.d.ts.map +1 -0
  40. package/dist/internal/machine/invocation.js +77 -0
  41. package/dist/internal/machine/invocation.js.map +1 -0
  42. package/dist/internal/machine/machine.d.ts +183 -0
  43. package/dist/internal/machine/machine.d.ts.map +1 -0
  44. package/dist/internal/machine/machine.js +552 -0
  45. package/dist/internal/machine/machine.js.map +1 -0
  46. package/dist/internal/machine/planner.d.ts +182 -0
  47. package/dist/internal/machine/planner.d.ts.map +1 -0
  48. package/dist/internal/machine/planner.js +1082 -0
  49. package/dist/internal/machine/planner.js.map +1 -0
  50. package/dist/internal/{machineProcess.d.ts → machine/process.d.ts} +11 -5
  51. package/dist/internal/machine/process.d.ts.map +1 -0
  52. package/dist/internal/machine/process.js +498 -0
  53. package/dist/internal/machine/process.js.map +1 -0
  54. package/dist/internal/machine/protocol.d.ts +34 -0
  55. package/dist/internal/machine/protocol.d.ts.map +1 -0
  56. package/dist/internal/machine/protocol.js +182 -0
  57. package/dist/internal/machine/protocol.js.map +1 -0
  58. package/dist/internal/machine/readiness.d.ts +4 -0
  59. package/dist/internal/machine/readiness.d.ts.map +1 -0
  60. package/dist/internal/machine/readiness.js +2 -0
  61. package/dist/internal/machine/readiness.js.map +1 -0
  62. package/dist/internal/machine/runtime.d.ts +255 -0
  63. package/dist/internal/machine/runtime.d.ts.map +1 -0
  64. package/dist/internal/machine/runtime.js +1489 -0
  65. package/dist/internal/machine/runtime.js.map +1 -0
  66. package/dist/internal/machine/serialization.d.ts +14 -0
  67. package/dist/internal/machine/serialization.d.ts.map +1 -0
  68. package/dist/internal/machine/serialization.js +338 -0
  69. package/dist/internal/machine/serialization.js.map +1 -0
  70. package/dist/internal/machine/stateDefinition.d.ts +15 -0
  71. package/dist/internal/machine/stateDefinition.d.ts.map +1 -0
  72. package/dist/internal/machine/stateDefinition.js +210 -0
  73. package/dist/internal/machine/stateDefinition.js.map +1 -0
  74. package/dist/internal/machine/symbols.d.ts +3 -0
  75. package/dist/internal/machine/symbols.d.ts.map +1 -0
  76. package/dist/internal/machine/symbols.js +3 -0
  77. package/dist/internal/machine/symbols.js.map +1 -0
  78. package/dist/internal/machine/topology.d.ts +98 -0
  79. package/dist/internal/machine/topology.d.ts.map +1 -0
  80. package/dist/internal/machine/topology.js +347 -0
  81. package/dist/internal/machine/topology.js.map +1 -0
  82. package/dist/internal/testing/machine/arbitrary.d.ts +36 -0
  83. package/dist/internal/testing/machine/arbitrary.d.ts.map +1 -0
  84. package/dist/internal/testing/machine/arbitrary.js +68 -0
  85. package/dist/internal/testing/machine/arbitrary.js.map +1 -0
  86. package/dist/internal/testing/machine/exploration.d.ts +30 -0
  87. package/dist/internal/testing/machine/exploration.d.ts.map +1 -0
  88. package/dist/internal/testing/machine/exploration.js +237 -0
  89. package/dist/internal/testing/machine/exploration.js.map +1 -0
  90. package/dist/internal/testing/machine/finiteModel.d.ts +322 -0
  91. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -0
  92. package/dist/internal/testing/machine/finiteModel.js +976 -0
  93. package/dist/internal/testing/machine/finiteModel.js.map +1 -0
  94. package/dist/internal/testing/machine/invariant.d.ts +31 -0
  95. package/dist/internal/testing/machine/invariant.d.ts.map +1 -0
  96. package/dist/internal/testing/machine/invariant.js +214 -0
  97. package/dist/internal/testing/machine/invariant.js.map +1 -0
  98. package/dist/internal/testing/machine/probe.d.ts +26 -0
  99. package/dist/internal/testing/machine/probe.d.ts.map +1 -0
  100. package/dist/internal/testing/machine/probe.js +45 -0
  101. package/dist/internal/testing/machine/probe.js.map +1 -0
  102. package/dist/internal/testing/machine/referenceModel.d.ts +203 -0
  103. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -0
  104. package/dist/internal/testing/machine/referenceModel.js +1012 -0
  105. package/dist/internal/testing/machine/referenceModel.js.map +1 -0
  106. package/dist/internal/testing/machine/runtime.d.ts +631 -0
  107. package/dist/internal/testing/machine/runtime.d.ts.map +1 -0
  108. package/dist/internal/testing/machine/runtime.js +726 -0
  109. package/dist/internal/testing/machine/runtime.js.map +1 -0
  110. package/dist/internal/testing/machine/runtimeInvariant.d.ts +32 -0
  111. package/dist/internal/testing/machine/runtimeInvariant.d.ts.map +1 -0
  112. package/dist/internal/testing/machine/runtimeInvariant.js +334 -0
  113. package/dist/internal/testing/machine/runtimeInvariant.js.map +1 -0
  114. package/dist/internal/testing/machine/trace.d.ts +17 -0
  115. package/dist/internal/testing/machine/trace.d.ts.map +1 -0
  116. package/dist/internal/testing/machine/trace.js +94 -0
  117. package/dist/internal/testing/machine/trace.js.map +1 -0
  118. package/dist/internal/testing/machine/verification.d.ts +36 -0
  119. package/dist/internal/testing/machine/verification.d.ts.map +1 -0
  120. package/dist/internal/testing/machine/verification.js +1375 -0
  121. package/dist/internal/testing/machine/verification.js.map +1 -0
  122. package/dist/testing/MachineTest.d.ts +1588 -0
  123. package/dist/testing/MachineTest.d.ts.map +1 -0
  124. package/dist/testing/MachineTest.js +406 -0
  125. package/dist/testing/MachineTest.js.map +1 -0
  126. package/dist/testing/index.d.ts +7 -0
  127. package/dist/testing/index.d.ts.map +1 -0
  128. package/dist/testing/index.js +7 -0
  129. package/dist/testing/index.js.map +1 -0
  130. package/dist/{ClusterMachine.d.ts → unstable/cluster/ClusterMachine.d.ts} +56 -45
  131. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -0
  132. package/dist/unstable/cluster/ClusterMachine.js +144 -0
  133. package/dist/unstable/cluster/ClusterMachine.js.map +1 -0
  134. package/dist/{cluster.js → unstable/cluster/index.d.ts} +1 -1
  135. package/dist/unstable/cluster/index.d.ts.map +1 -0
  136. package/dist/{cluster.d.ts → unstable/cluster/index.js} +1 -1
  137. package/dist/unstable/cluster/index.js.map +1 -0
  138. package/dist/{AtomMachine.d.ts → unstable/reactivity/AtomMachine.d.ts} +116 -50
  139. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -0
  140. package/dist/unstable/reactivity/AtomMachine.js +164 -0
  141. package/dist/unstable/reactivity/AtomMachine.js.map +1 -0
  142. package/dist/{reactivity.js → unstable/reactivity/index.d.ts} +1 -1
  143. package/dist/unstable/reactivity/index.d.ts.map +1 -0
  144. package/dist/{reactivity.d.ts → unstable/reactivity/index.js} +1 -1
  145. package/dist/unstable/reactivity/index.js.map +1 -0
  146. package/docs/agent-guide.md +316 -31
  147. package/package.json +28 -10
  148. package/dist/AtomMachine.d.ts.map +0 -1
  149. package/dist/AtomMachine.js.map +0 -1
  150. package/dist/ClusterMachine.d.ts.map +0 -1
  151. package/dist/ClusterMachine.js.map +0 -1
  152. package/dist/cluster.d.ts.map +0 -1
  153. package/dist/cluster.js.map +0 -1
  154. package/dist/internal/machineErrors.d.ts.map +0 -1
  155. package/dist/internal/machineErrors.js.map +0 -1
  156. package/dist/internal/machineModel.d.ts.map +0 -1
  157. package/dist/internal/machineModel.js.map +0 -1
  158. package/dist/internal/machinePlanner.d.ts +0 -74
  159. package/dist/internal/machinePlanner.d.ts.map +0 -1
  160. package/dist/internal/machinePlanner.js +0 -797
  161. package/dist/internal/machinePlanner.js.map +0 -1
  162. package/dist/internal/machineProcess.d.ts.map +0 -1
  163. package/dist/internal/machineProcess.js +0 -181
  164. package/dist/internal/machineProcess.js.map +0 -1
  165. package/dist/internal/machineRuntime.d.ts +0 -125
  166. package/dist/internal/machineRuntime.d.ts.map +0 -1
  167. package/dist/internal/machineRuntime.js +0 -365
  168. package/dist/internal/machineRuntime.js.map +0 -1
  169. package/dist/reactivity.d.ts.map +0 -1
  170. package/dist/reactivity.js.map +0 -1
@@ -0,0 +1,726 @@
1
+ /**
2
+ * Effect-native command-model testing for live machine references.
3
+ *
4
+ * @since 0.4.0
5
+ */
6
+ import * as Cause from "effect/Cause";
7
+ import * as Data from "effect/Data";
8
+ import * as Deferred from "effect/Deferred";
9
+ import * as Duration from "effect/Duration";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Inspectable from "effect/Inspectable";
12
+ import * as Queue from "effect/Queue";
13
+ import * as Stream from "effect/Stream";
14
+ import { FastCheck, TestClock } from "effect/testing";
15
+ import * as Machine from "../../../Machine.js";
16
+ import { toArbitraryWithReport } from "./arbitrary.js";
17
+ import { assertRuntimeInvariants } from "./runtimeInvariant.js";
18
+ /**
19
+ * Constructs a command that sends one public event.
20
+ *
21
+ * @category constructors
22
+ * @since 0.4.0
23
+ */
24
+ export const sendCommand = (event) => ({
25
+ _tag: "Send",
26
+ event
27
+ });
28
+ /**
29
+ * Constructs a command that advances Effect's `TestClock`.
30
+ *
31
+ * @category constructors
32
+ * @since 0.4.0
33
+ */
34
+ export const advanceCommand = (duration) => ({
35
+ _tag: "Advance",
36
+ duration
37
+ });
38
+ /**
39
+ * Constructs an idempotent command that stops the machine.
40
+ *
41
+ * @category constructors
42
+ * @since 0.4.0
43
+ */
44
+ export const stopCommand = () => ({ _tag: "Stop" });
45
+ /**
46
+ * Constructs a no-op command used to synchronize with work enqueued by earlier
47
+ * commands. Its behavior is selected by the reference-model step.
48
+ *
49
+ * @category constructors
50
+ * @since 0.4.0
51
+ */
52
+ export const checkpointCommand = (label) => ({
53
+ _tag: "Checkpoint",
54
+ label
55
+ });
56
+ /**
57
+ * Constructors for runtime synchronization policies.
58
+ *
59
+ * @category constructors
60
+ * @since 0.4.0
61
+ */
62
+ export const RuntimeSynchronization = {
63
+ none: { _tag: "None" },
64
+ current: { _tag: "Current" },
65
+ next: { _tag: "Next" },
66
+ until: (predicate) => ({ _tag: "Until", predicate })
67
+ };
68
+ /**
69
+ * Failure raised when an expected public change stream observation is absent.
70
+ *
71
+ * @category errors
72
+ * @since 0.4.0
73
+ */
74
+ export class RuntimeObservationError extends Data.TaggedError("MachineTestRuntimeObservationError") {
75
+ }
76
+ /**
77
+ * A typed command-model failure retaining the successfully checked prefix.
78
+ *
79
+ * @category errors
80
+ * @since 0.4.0
81
+ */
82
+ export class RuntimeCommandFailure extends Data.TaggedError("MachineTestRuntimeCommandFailure") {
83
+ }
84
+ /**
85
+ * A typed causal command-model failure retaining the successfully checked
86
+ * prefix and exact attempted command.
87
+ *
88
+ * @category errors
89
+ * @since 0.4.0
90
+ */
91
+ export class CausalRuntimeCommandFailure extends Data.TaggedError("MachineTestCausalRuntimeCommandFailure") {
92
+ }
93
+ const makeFailure = (options) => new RuntimeCommandFailure({
94
+ ...options,
95
+ prefix: options.prefix.slice(),
96
+ attempted: options.attempted
97
+ });
98
+ const executeCommand = (ref, command) => {
99
+ switch (command._tag) {
100
+ case "Send":
101
+ return ref.send(command.event).pipe(Effect.match({
102
+ onFailure: (error) => ({ _tag: "SendRejected", error }),
103
+ onSuccess: () => ({ _tag: "SendAccepted" })
104
+ }));
105
+ case "Advance":
106
+ return TestClock.adjust(command.duration).pipe(Effect.as({ _tag: "ClockAdvanced" }));
107
+ case "Stop":
108
+ return ref.stop.pipe(Effect.as({ _tag: "Stopped" }));
109
+ case "Checkpoint":
110
+ return Effect.succeed({ _tag: "Checkpoint" });
111
+ }
112
+ };
113
+ const synchronize = (ref, queue, policy, index, timeout) => {
114
+ const wait = (synchronization, effect) => TestClock.withLive(effect.pipe(Effect.timeout(timeout))).pipe(Effect.mapError((cause) => cause instanceof RuntimeObservationError
115
+ ? cause
116
+ : new RuntimeObservationError({
117
+ index,
118
+ synchronization,
119
+ reason: "timeout",
120
+ message: `timed out after ${Duration.toMillis(timeout)}ms waiting for the expected published snapshot`
121
+ })));
122
+ switch (policy._tag) {
123
+ case "None":
124
+ return Effect.succeed({ snapshot: undefined, published: [] });
125
+ case "Current":
126
+ return ref.snapshot.pipe(Effect.map((snapshot) => ({ snapshot, published: [] })));
127
+ case "Next":
128
+ return wait("Next", Queue.take(queue).pipe(Effect.flatMap((entry) => entry._tag === "Snapshot"
129
+ ? Effect.succeed({ snapshot: entry.snapshot, published: [entry.snapshot] })
130
+ : Effect.fail(new RuntimeObservationError({
131
+ index,
132
+ synchronization: "Next",
133
+ reason: "ended",
134
+ message: "the machine changes stream ended before publishing the expected snapshot"
135
+ })))));
136
+ case "Until":
137
+ return wait("Until", Effect.gen(function* () {
138
+ const published = [];
139
+ while (true) {
140
+ const entry = yield* Queue.take(queue);
141
+ if (entry._tag === "End") {
142
+ return yield* Effect.fail(new RuntimeObservationError({
143
+ index,
144
+ synchronization: "Until",
145
+ reason: "ended",
146
+ message: "the machine changes stream ended before a published snapshot matched the predicate"
147
+ }));
148
+ }
149
+ published.push(entry.snapshot);
150
+ if (policy.predicate(entry.snapshot)) {
151
+ return { snapshot: entry.snapshot, published };
152
+ }
153
+ }
154
+ }));
155
+ }
156
+ };
157
+ const executeCausalCommand = (probe, command) => {
158
+ switch (command._tag) {
159
+ case "Send":
160
+ return Effect.matchEffect(probe.sendAndAwait(command.event), {
161
+ onFailure: (error) => error instanceof Machine.StoppedError
162
+ ? probe.ref.snapshot.pipe(Effect.flatMap((snapshot) => snapshot.status === "stopped"
163
+ ? Effect.succeed({ _tag: "SendRejected", error })
164
+ : Effect.fail(error)))
165
+ : Effect.fail(error),
166
+ onSuccess: (step) => Effect.succeed({ _tag: "SendProcessed", step })
167
+ });
168
+ case "Advance":
169
+ return TestClock.adjust(command.duration).pipe(Effect.as({ _tag: "ClockAdvanced" }));
170
+ case "Stop":
171
+ return probe.ref.stop.pipe(Effect.as({ _tag: "Stopped" }));
172
+ case "Checkpoint":
173
+ return Effect.succeed({ _tag: "Checkpoint" });
174
+ }
175
+ };
176
+ const awaitCausal = (ref, policy, index, timeout) => {
177
+ if (policy._tag === "None") {
178
+ return ref.snapshot.pipe(Effect.map((snapshot) => ({ snapshot, awaited: [] })));
179
+ }
180
+ const observation = Effect.scoped(Effect.gen(function* () {
181
+ const changes = yield* Queue.unbounded();
182
+ yield* ref.changes.pipe(Stream.runForEach((snapshot) => Queue.offer(changes, { _tag: "Snapshot", snapshot })), Effect.ensuring(Queue.offer(changes, { _tag: "End" })), Effect.forkScoped({ startImmediately: true }));
183
+ const awaited = [];
184
+ while (true) {
185
+ const entry = yield* Queue.take(changes);
186
+ if (entry._tag === "End") {
187
+ return yield* Effect.fail(new RuntimeObservationError({
188
+ index,
189
+ synchronization: "Until",
190
+ reason: "ended",
191
+ message: "the machine changes stream ended before an awaited snapshot matched the predicate"
192
+ }));
193
+ }
194
+ awaited.push(entry.snapshot);
195
+ if (policy.predicate(entry.snapshot))
196
+ return { snapshot: entry.snapshot, awaited };
197
+ }
198
+ }));
199
+ return TestClock.withLive(observation.pipe(Effect.timeout(timeout))).pipe(Effect.mapError((cause) => cause instanceof RuntimeObservationError
200
+ ? cause
201
+ : new RuntimeObservationError({
202
+ index,
203
+ synchronization: "Until",
204
+ reason: "timeout",
205
+ message: `timed out after ${Duration.toMillis(timeout)}ms waiting for an awaited runtime snapshot`
206
+ })));
207
+ };
208
+ /**
209
+ * Runs typed commands against a live `MachineRef` and checks them against a
210
+ * supplied Effect-native reference model.
211
+ *
212
+ * The runner observes only public `MachineRef` behavior. In particular, a
213
+ * successful send means enqueue acceptance, not completed processing. A model
214
+ * must request `Next`/`Until` only when it predicts a publication, or use an
215
+ * explicit checkpoint to drain previously enqueued work. Machine emissions can
216
+ * be captured by the runtime service used by the machine and returned from the
217
+ * optional `inspect` effect.
218
+ *
219
+ * Typed failures and defects from model transitions, command execution,
220
+ * inspection, and assertions are retained as full `Cause` values. A cause
221
+ * containing only interruption is propagated as interruption so cancelling a
222
+ * property run cannot be mistaken for a machine counterexample.
223
+ *
224
+ * @category constructors
225
+ * @since 0.4.0
226
+ */
227
+ export const runEnqueuedCommands = (ref, commands, options) => Effect.scoped(Effect.gen(function* () {
228
+ const sequence = Array.from(commands);
229
+ const observationTimeout = options.observationTimeout ?? "1 second";
230
+ const observationTimeoutMillis = Duration.toMillis(observationTimeout);
231
+ if (!Number.isFinite(observationTimeoutMillis) || observationTimeoutMillis < 0) {
232
+ return yield* Effect.die(new Error("MachineTest.runEnqueuedCommands expected observationTimeout to be a finite non-negative duration"));
233
+ }
234
+ const changes = yield* Queue.unbounded();
235
+ const ready = yield* Deferred.make();
236
+ yield* ref.changes.pipe(Stream.runForEach((snapshot) => Queue.offer(changes, { _tag: "Snapshot", snapshot }).pipe(Effect.andThen(Deferred.succeed(ready, undefined)), Effect.asVoid)), Effect.ensuring(Deferred.succeed(ready, undefined).pipe(Effect.andThen(Queue.offer(changes, { _tag: "End" })), Effect.asVoid)), Effect.forkScoped({ startImmediately: true }));
237
+ yield* Deferred.await(ready);
238
+ const initialEntry = yield* Queue.take(changes);
239
+ const initial = initialEntry._tag === "Snapshot" ? initialEntry.snapshot : yield* ref.snapshot;
240
+ const records = [];
241
+ let model = options.initialModel;
242
+ let lastSynchronized = initial;
243
+ let outstandingWorkUnknown = false;
244
+ const capture = (options) => Effect.catchCause(Effect.suspend(options.effect), (cause) => Cause.hasInterruptsOnly(cause)
245
+ ? Effect.failCause(cause)
246
+ : Effect.fail(makeFailure({
247
+ phase: options.phase,
248
+ index: options.index,
249
+ command: options.command,
250
+ cause,
251
+ prefix: records,
252
+ ...(options.attempted === undefined ? {} : { attempted: options.attempted })
253
+ })));
254
+ for (let index = 0; index < sequence.length; index++) {
255
+ const command = sequence[index];
256
+ const step = yield* capture({
257
+ phase: "model",
258
+ index,
259
+ command,
260
+ effect: () => options.transition(model, command, index)
261
+ });
262
+ model = step.model;
263
+ const result = yield* capture({
264
+ phase: "execution",
265
+ index,
266
+ command,
267
+ effect: () => executeCommand(ref, command)
268
+ });
269
+ const attemptedBeforeObservation = {
270
+ index,
271
+ command,
272
+ model,
273
+ expected: step.expected,
274
+ actual: {
275
+ result,
276
+ snapshot: undefined,
277
+ published: [],
278
+ inspected: undefined
279
+ }
280
+ };
281
+ const synchronized = yield* capture({
282
+ phase: "observation",
283
+ index,
284
+ command,
285
+ effect: () => synchronize(ref, changes, step.synchronize, index, observationTimeout),
286
+ attempted: attemptedBeforeObservation
287
+ });
288
+ const terminal = synchronized.snapshot !== undefined && synchronized.snapshot.status !== "active";
289
+ const previouslyOutstanding = outstandingWorkUnknown;
290
+ switch (step.synchronize._tag) {
291
+ case "None":
292
+ if (command._tag === "Advance" || command._tag === "Stop" ||
293
+ (command._tag === "Send" && result._tag === "SendAccepted"))
294
+ outstandingWorkUnknown = true;
295
+ break;
296
+ case "Next":
297
+ if (synchronized.snapshot !== undefined)
298
+ lastSynchronized = synchronized.snapshot;
299
+ outstandingWorkUnknown = terminal
300
+ ? false
301
+ : previouslyOutstanding || command._tag === "Advance" ||
302
+ (command._tag === "Send" && result._tag === "SendAccepted");
303
+ break;
304
+ case "Until":
305
+ if (synchronized.snapshot !== undefined)
306
+ lastSynchronized = synchronized.snapshot;
307
+ outstandingWorkUnknown = false;
308
+ break;
309
+ case "Current":
310
+ if (terminal)
311
+ outstandingWorkUnknown = false;
312
+ if (!outstandingWorkUnknown && synchronized.snapshot !== undefined)
313
+ lastSynchronized = synchronized.snapshot;
314
+ break;
315
+ }
316
+ const inspectionContext = {
317
+ index,
318
+ command,
319
+ result,
320
+ ref,
321
+ snapshot: synchronized.snapshot,
322
+ published: synchronized.published
323
+ };
324
+ const attemptedBeforeInspection = {
325
+ ...attemptedBeforeObservation,
326
+ actual: {
327
+ result,
328
+ snapshot: synchronized.snapshot,
329
+ published: synchronized.published,
330
+ inspected: undefined
331
+ }
332
+ };
333
+ const inspected = options.inspect === undefined
334
+ ? undefined
335
+ : yield* capture({
336
+ phase: "inspection",
337
+ index,
338
+ command,
339
+ effect: () => options.inspect(inspectionContext),
340
+ attempted: attemptedBeforeInspection
341
+ });
342
+ const actual = {
343
+ result,
344
+ snapshot: synchronized.snapshot,
345
+ published: synchronized.published,
346
+ inspected
347
+ };
348
+ const record = {
349
+ index,
350
+ command,
351
+ model,
352
+ expected: step.expected,
353
+ actual
354
+ };
355
+ yield* capture({
356
+ phase: "assertion",
357
+ index,
358
+ command,
359
+ effect: () => options.assert({
360
+ ...inspectionContext,
361
+ model,
362
+ expected: step.expected,
363
+ actual
364
+ }),
365
+ attempted: record
366
+ });
367
+ records.push(record);
368
+ }
369
+ return {
370
+ commands: sequence,
371
+ initial,
372
+ records,
373
+ finalModel: model,
374
+ final: lastSynchronized,
375
+ synchronized: !outstandingWorkUnknown
376
+ };
377
+ }));
378
+ /**
379
+ * Compatibility alias for enqueue-oriented runtime command execution.
380
+ *
381
+ * @deprecated Use `runEnqueuedCommands`. This compatibility name does not
382
+ * expose whether sends are merely enqueued or causally processed.
383
+ *
384
+ * @category constructors
385
+ * @since 0.4.0
386
+ */
387
+ export const runRuntimeCommands = runEnqueuedCommands;
388
+ /**
389
+ * Runs typed commands against a probe and checks them against an Effect-native
390
+ * reference model.
391
+ *
392
+ * Every accepted `Send` completes its exact managed runtime macrostep before
393
+ * inspection, assertion, and the next command. Use `probe.await.until` only
394
+ * for later asynchronous work such as timer, invoke, or child delivery.
395
+ * Processing failures are attributed to the exact submitted command and retain
396
+ * the successfully checked prefix for FastCheck shrinking and replay.
397
+ *
398
+ * Use `runEnqueuedCommands` instead when the behavior under test intentionally
399
+ * depends on burst enqueueing or outstanding mailbox work.
400
+ *
401
+ * @category constructors
402
+ * @since 0.4.0
403
+ */
404
+ export const runCausalCommands = (probe, commands, options) => Effect.gen(function* () {
405
+ const sequence = Array.from(commands);
406
+ const observationTimeout = options.observationTimeout ?? "1 second";
407
+ const observationTimeoutMillis = Duration.toMillis(observationTimeout);
408
+ if (!Number.isFinite(observationTimeoutMillis) || observationTimeoutMillis < 0) {
409
+ return yield* Effect.die(new Error("MachineTest.runCausalCommands expected observationTimeout to be a finite non-negative duration"));
410
+ }
411
+ const initial = yield* probe.ref.snapshot;
412
+ const records = [];
413
+ let model = options.initialModel;
414
+ let final = initial;
415
+ const capture = (captureOptions) => Effect.catchCause(Effect.suspend(captureOptions.effect), (cause) => Cause.hasInterruptsOnly(cause)
416
+ ? Effect.failCause(cause)
417
+ : Effect.fail(new CausalRuntimeCommandFailure({
418
+ phase: captureOptions.phase,
419
+ index: captureOptions.index,
420
+ command: captureOptions.command,
421
+ cause,
422
+ prefix: records.slice(),
423
+ attempted: captureOptions.attempted
424
+ })));
425
+ for (let index = 0; index < sequence.length; index++) {
426
+ const command = sequence[index];
427
+ const step = yield* capture({
428
+ phase: "model",
429
+ index,
430
+ command,
431
+ effect: () => options.transition(model, command, index)
432
+ });
433
+ model = step.model;
434
+ const attemptedBeforeExecution = {
435
+ index,
436
+ command,
437
+ model,
438
+ expected: step.expected,
439
+ result: undefined,
440
+ snapshot: undefined,
441
+ awaited: [],
442
+ inspected: undefined
443
+ };
444
+ const result = yield* capture({
445
+ phase: "execution",
446
+ index,
447
+ command,
448
+ effect: () => executeCausalCommand(probe, command),
449
+ attempted: attemptedBeforeExecution
450
+ });
451
+ const attemptedAfterExecution = {
452
+ ...attemptedBeforeExecution,
453
+ result
454
+ };
455
+ const observation = yield* capture({
456
+ phase: "observation",
457
+ index,
458
+ command,
459
+ effect: () => awaitCausal(probe.ref, step.await ?? { _tag: "None" }, index, observationTimeout),
460
+ attempted: attemptedAfterExecution
461
+ });
462
+ final = observation.snapshot;
463
+ const inspectionContext = {
464
+ index,
465
+ command,
466
+ result,
467
+ probe,
468
+ ref: probe.ref,
469
+ snapshot: observation.snapshot,
470
+ awaited: observation.awaited
471
+ };
472
+ const attemptedAfterObservation = {
473
+ ...attemptedAfterExecution,
474
+ snapshot: observation.snapshot,
475
+ awaited: observation.awaited
476
+ };
477
+ const inspected = options.inspect === undefined
478
+ ? undefined
479
+ : yield* capture({
480
+ phase: "inspection",
481
+ index,
482
+ command,
483
+ effect: () => options.inspect(inspectionContext),
484
+ attempted: attemptedAfterObservation
485
+ });
486
+ const actual = {
487
+ result,
488
+ snapshot: observation.snapshot,
489
+ awaited: observation.awaited,
490
+ inspected
491
+ };
492
+ const record = {
493
+ index,
494
+ command,
495
+ model,
496
+ expected: step.expected,
497
+ actual
498
+ };
499
+ yield* capture({
500
+ phase: "assertion",
501
+ index,
502
+ command,
503
+ effect: () => options.assert({
504
+ ...inspectionContext,
505
+ model,
506
+ expected: step.expected,
507
+ actual
508
+ }),
509
+ attempted: {
510
+ ...attemptedAfterObservation,
511
+ inspected
512
+ }
513
+ });
514
+ records.push(record);
515
+ }
516
+ return {
517
+ commands: sequence,
518
+ initial,
519
+ records,
520
+ finalModel: model,
521
+ final
522
+ };
523
+ });
524
+ /**
525
+ * Causally executes commands and checks reusable runtime invariants without
526
+ * requiring a dummy reference model. Use `runCausalCommands` when exact
527
+ * expected results come from an application model.
528
+ *
529
+ * @category constructors
530
+ * @since 0.4.0
531
+ */
532
+ export const verifyCausalCommands = (probe, commands, options) => Effect.gen(function* () {
533
+ const transcript = yield* runCausalCommands(probe, commands, {
534
+ initialModel: undefined,
535
+ ...(options.observationTimeout === undefined ? {} : { observationTimeout: options.observationTimeout }),
536
+ transition: (_model, command, index) => Effect.sync(() => ({
537
+ model: undefined,
538
+ expected: undefined,
539
+ ...(options.await === undefined ? {} : { await: options.await({ index, command, probe }) })
540
+ })),
541
+ assert: () => Effect.void
542
+ });
543
+ const evidence = {
544
+ commands: transcript.commands,
545
+ initial: transcript.initial,
546
+ records: transcript.records.map(({ actual, command, index }) => ({ index, command, actual })),
547
+ final: transcript.final
548
+ };
549
+ yield* assertRuntimeInvariants(probe.machine, evidence, options.invariants);
550
+ return evidence;
551
+ });
552
+ const validateCommandLength = (name, value) => {
553
+ if (!Number.isSafeInteger(value) || value < 0) {
554
+ throw new Error(`MachineTest.runtimeCommands expected ${name} to be a non-negative safe integer`);
555
+ }
556
+ };
557
+ /**
558
+ * Derives a shrinkable command sequence from public event schemas and explicit
559
+ * clock/stop/checkpoint command choices.
560
+ *
561
+ * This deliberately returns ordinary Effect FastCheck arbitraries instead of
562
+ * adapting the runner through `asyncModelRun`: the latter requires Promise
563
+ * callbacks and would erase Effect error and service channels.
564
+ *
565
+ * @category constructors
566
+ * @since 0.4.0
567
+ */
568
+ export const runtimeCommands = (machine, options = {}) => {
569
+ const minCommands = options.minCommands ?? 0;
570
+ const maxCommands = options.maxCommands ?? 50;
571
+ validateCommandLength("minCommands", minCommands);
572
+ validateCommandLength("maxCommands", maxCommands);
573
+ if (minCommands > maxCommands) {
574
+ throw new Error("MachineTest.runtimeCommands expected minCommands to be less than or equal to maxCommands");
575
+ }
576
+ const reports = [];
577
+ const eventArbitraries = options.eventArbitrary === undefined
578
+ ? machine.events.map((schema) => {
579
+ const derived = toArbitraryWithReport(schema);
580
+ reports.push(derived.report);
581
+ return derived.value;
582
+ })
583
+ : [];
584
+ const eventArbitrary = options.eventArbitrary ?? (eventArbitraries.length === 0
585
+ ? undefined
586
+ : FastCheck.oneof(...eventArbitraries));
587
+ const commandArbitraries = [];
588
+ if (eventArbitrary !== undefined)
589
+ commandArbitraries.push(eventArbitrary.map(sendCommand));
590
+ if (options.includeAdvance !== false) {
591
+ const advanceArbitrary = options.advanceArbitrary ?? FastCheck.nat({ max: 60_000 });
592
+ commandArbitraries.push(advanceArbitrary.map(advanceCommand));
593
+ }
594
+ if (options.includeStop !== false)
595
+ commandArbitraries.push(FastCheck.constant(stopCommand()));
596
+ if (options.includeCheckpoint !== false)
597
+ commandArbitraries.push(FastCheck.constant(checkpointCommand()));
598
+ commandArbitraries.push(...options.additionalCommands ?? []);
599
+ if (commandArbitraries.length === 0) {
600
+ if (minCommands > 0) {
601
+ throw new Error("MachineTest.runtimeCommands cannot generate a non-empty command sequence without commands");
602
+ }
603
+ return {
604
+ arbitrary: FastCheck.constant([]),
605
+ diagnostics: {
606
+ events: options.eventArbitrary !== undefined ? "override" : eventArbitraries.length === 0 ? "none" : "schema",
607
+ schemaReports: reports,
608
+ includesAdvance: false,
609
+ includesStop: false,
610
+ includesCheckpoint: false
611
+ }
612
+ };
613
+ }
614
+ return {
615
+ arbitrary: FastCheck.array(FastCheck.oneof(...commandArbitraries), {
616
+ minLength: minCommands,
617
+ maxLength: maxCommands
618
+ }),
619
+ diagnostics: {
620
+ events: options.eventArbitrary !== undefined ? "override" : eventArbitraries.length === 0 ? "none" : "schema",
621
+ schemaReports: reports,
622
+ includesAdvance: options.includeAdvance !== false,
623
+ includesStop: options.includeStop !== false,
624
+ includesCheckpoint: options.includeCheckpoint !== false
625
+ }
626
+ };
627
+ };
628
+ /**
629
+ * Formats a runtime transcript or failure as replayable line-oriented evidence.
630
+ *
631
+ * @category formatting
632
+ * @since 0.4.0
633
+ */
634
+ export const formatEnqueuedTranscript = (value) => {
635
+ const failure = value instanceof RuntimeCommandFailure;
636
+ const records = failure ? value.prefix : value.records;
637
+ const lines = [
638
+ `commands: ${Inspectable.toStringUnknown(failure ?
639
+ [
640
+ ...value.prefix.map((record) => record.command),
641
+ value.command
642
+ ] :
643
+ value.commands, 0)}`
644
+ ];
645
+ for (const record of records) {
646
+ lines.push(`command ${record.index}: command=${Inspectable.toStringUnknown(record.command, 0)} ` +
647
+ `model=${Inspectable.toStringUnknown(record.model, 0)} ` +
648
+ `expected=${Inspectable.toStringUnknown(record.expected, 0)} ` +
649
+ `result=${Inspectable.toStringUnknown(record.actual.result, 0)} ` +
650
+ `snapshot=${Inspectable.toStringUnknown(record.actual.snapshot, 0)} ` +
651
+ `published=${Inspectable.toStringUnknown(record.actual.published, 0)} ` +
652
+ `inspected=${Inspectable.toStringUnknown(record.actual.inspected, 0)}`);
653
+ }
654
+ if (failure) {
655
+ if (value.attempted !== undefined) {
656
+ lines.push(`attempted ${value.attempted.index}: command=${Inspectable.toStringUnknown(value.attempted.command, 0)} ` +
657
+ `model=${Inspectable.toStringUnknown(value.attempted.model, 0)} ` +
658
+ `expected=${Inspectable.toStringUnknown(value.attempted.expected, 0)} ` +
659
+ `result=${Inspectable.toStringUnknown(value.attempted.actual.result, 0)} ` +
660
+ `snapshot=${Inspectable.toStringUnknown(value.attempted.actual.snapshot, 0)} ` +
661
+ `published=${Inspectable.toStringUnknown(value.attempted.actual.published, 0)} ` +
662
+ `inspected=${Inspectable.toStringUnknown(value.attempted.actual.inspected, 0)}`);
663
+ }
664
+ lines.push(`failure: phase=${value.phase} index=${value.index} command=${Inspectable.toStringUnknown(value.command, 0)} ` +
665
+ `cause=${Inspectable.toStringUnknown(value.cause, 0)}`);
666
+ }
667
+ else {
668
+ lines.push(`final: synchronized=${String(value.synchronized)} snapshot=${Inspectable.toStringUnknown(value.final, 0)}`);
669
+ }
670
+ return lines.join("\n");
671
+ };
672
+ /**
673
+ * Compatibility alias for enqueue-oriented transcript formatting.
674
+ *
675
+ * @deprecated Use `formatEnqueuedTranscript`.
676
+ *
677
+ * @category formatting
678
+ * @since 0.4.0
679
+ */
680
+ export const formatRuntimeTranscript = formatEnqueuedTranscript;
681
+ /**
682
+ * Formats a causal runtime transcript or failure as replayable line-oriented
683
+ * evidence, including exact probe steps and explicit asynchronous observations.
684
+ *
685
+ * @category formatting
686
+ * @since 0.4.0
687
+ */
688
+ export const formatCausalTranscript = (value) => {
689
+ const failure = value instanceof CausalRuntimeCommandFailure;
690
+ const records = failure ? value.prefix : value.records;
691
+ const lines = [
692
+ `commands: ${Inspectable.toStringUnknown(failure
693
+ ? [
694
+ ...value.prefix.map((record) => record.command),
695
+ value.command
696
+ ]
697
+ : value.commands, 0)}`
698
+ ];
699
+ for (const record of records) {
700
+ lines.push(`command ${record.index}: command=${Inspectable.toStringUnknown(record.command, 0)} ` +
701
+ `model=${Inspectable.toStringUnknown(record.model, 0)} ` +
702
+ `expected=${Inspectable.toStringUnknown(record.expected, 0)} ` +
703
+ `result=${Inspectable.toStringUnknown(record.actual.result, 0)} ` +
704
+ `snapshot=${Inspectable.toStringUnknown(record.actual.snapshot, 0)} ` +
705
+ `awaited=${Inspectable.toStringUnknown(record.actual.awaited, 0)} ` +
706
+ `inspected=${Inspectable.toStringUnknown(record.actual.inspected, 0)}`);
707
+ }
708
+ if (failure) {
709
+ if (value.attempted !== undefined) {
710
+ lines.push(`attempted ${value.attempted.index}: command=${Inspectable.toStringUnknown(value.attempted.command, 0)} ` +
711
+ `model=${Inspectable.toStringUnknown(value.attempted.model, 0)} ` +
712
+ `expected=${Inspectable.toStringUnknown(value.attempted.expected, 0)} ` +
713
+ `result=${Inspectable.toStringUnknown(value.attempted.result, 0)} ` +
714
+ `snapshot=${Inspectable.toStringUnknown(value.attempted.snapshot, 0)} ` +
715
+ `awaited=${Inspectable.toStringUnknown(value.attempted.awaited, 0)} ` +
716
+ `inspected=${Inspectable.toStringUnknown(value.attempted.inspected, 0)}`);
717
+ }
718
+ lines.push(`failure: phase=${value.phase} index=${value.index} command=${Inspectable.toStringUnknown(value.command, 0)} ` +
719
+ `cause=${Inspectable.toStringUnknown(value.cause, 0)}`);
720
+ }
721
+ else {
722
+ lines.push(`final: snapshot=${Inspectable.toStringUnknown(value.final, 0)}`);
723
+ }
724
+ return lines.join("\n");
725
+ };
726
+ //# sourceMappingURL=runtime.js.map