@demlik/tea 0.15.0 → 0.17.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.
- package/README.md +30 -7
- package/dist/agent/index.d.ts +49 -37
- package/dist/agent/index.js +12 -8
- package/dist/chunk-4LPY67E4.js +18 -0
- package/dist/chunk-4LPY67E4.js.map +1 -0
- package/dist/chunk-4NTCPTNQ.js +64 -0
- package/dist/chunk-4NTCPTNQ.js.map +1 -0
- package/dist/{chunk-3B5X7SBB.js → chunk-4PXNZ5UY.js} +11 -9
- package/dist/chunk-4PXNZ5UY.js.map +1 -0
- package/dist/{chunk-AYS5N6WY.js → chunk-6XBEYZME.js} +62 -172
- package/dist/chunk-6XBEYZME.js.map +1 -0
- package/dist/chunk-C4U5D5HP.js +380 -0
- package/dist/chunk-C4U5D5HP.js.map +1 -0
- package/dist/{chunk-QPEVCJ5Q.js → chunk-DFUEAA6L.js} +13 -6
- package/dist/chunk-DFUEAA6L.js.map +1 -0
- package/dist/{chunk-22POWGFA.js → chunk-GWWCCSDH.js} +46 -20
- package/dist/chunk-GWWCCSDH.js.map +1 -0
- package/dist/chunk-K4ETBNKS.js +733 -0
- package/dist/chunk-K4ETBNKS.js.map +1 -0
- package/dist/chunk-KWNCM6XM.js +539 -0
- package/dist/chunk-KWNCM6XM.js.map +1 -0
- package/dist/{chunk-V46HSVCK.js → chunk-LYVOGOBC.js} +3 -3
- package/dist/{chunk-V46HSVCK.js.map → chunk-LYVOGOBC.js.map} +1 -1
- package/dist/{chunk-VTQI2NHP.js → chunk-OPH6Y4UT.js} +9 -8
- package/dist/chunk-OPH6Y4UT.js.map +1 -0
- package/dist/{chunk-Q5CQTBFA.js → chunk-OYR7ENKU.js} +20 -30
- package/dist/chunk-OYR7ENKU.js.map +1 -0
- package/dist/{chunk-MYXFNWAJ.js → chunk-QNGYQMZB.js} +4 -4
- package/dist/chunk-QNGYQMZB.js.map +1 -0
- package/dist/chunk-QWIPH4YH.js +98 -0
- package/dist/chunk-QWIPH4YH.js.map +1 -0
- package/dist/{chunk-TCLBCERH.js → chunk-XBZQT2TD.js} +238 -212
- package/dist/chunk-XBZQT2TD.js.map +1 -0
- package/dist/{core-DnzqWy6u.d.ts → core-rDbOaJTN.d.ts} +236 -140
- package/dist/do/index.d.ts +37 -33
- package/dist/do/index.js +21 -14
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-CWzA-o2B.d.ts → durable-effects-CNaw97Be.d.ts} +1 -1
- package/dist/effect/index.d.ts +151 -0
- package/dist/effect/index.js +98 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/extension/index.d.ts +6 -7
- package/dist/extension/index.js +3 -2
- package/dist/extension/index.js.map +1 -1
- package/dist/flow/index.d.ts +68 -59
- package/dist/flow/index.js +32 -27
- package/dist/flow/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +6 -7
- package/dist/idempotency/index.js +4 -2
- package/dist/{index-KXqTRJyb.d.ts → index-BOrsbdAf.d.ts} +19 -96
- package/dist/{index-DrIRb_Ch.d.ts → index-B_BxplWi.d.ts} +1 -1
- package/dist/index-CWmsXNdX.d.ts +207 -0
- package/dist/{index-C2DwkjlP.d.ts → index-Cberb5Rr.d.ts} +57 -45
- package/dist/{index-CSyMYDAB.d.ts → index-CcrKLtse.d.ts} +1 -1
- package/dist/{index-CuNO9m8i.d.ts → index-Cx0F2M34.d.ts} +14 -16
- package/dist/{index-HSYgG2Yt.d.ts → index-DaX_P35Q.d.ts} +247 -211
- package/dist/{index-CA_7I5g9.d.ts → index-q1PoukOZ.d.ts} +137 -246
- package/dist/index.d.ts +121 -385
- package/dist/index.js +2 -1
- package/dist/jev/index.d.ts +133 -97
- package/dist/jev/index.js +205 -161
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-DGlPWCWI.d.ts → machine-BcfCFMYF.d.ts} +171 -288
- package/dist/machine-viz/index.d.ts +5 -5
- package/dist/machine-viz/index.js +8 -4
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +3 -4
- package/dist/mem/index.js +2 -1
- package/dist/mem/index.js.map +1 -1
- package/dist/node/index.d.ts +119 -78
- package/dist/node/index.js +52 -38
- package/dist/node/index.js.map +1 -1
- package/dist/paginate/index.d.ts +27 -22
- package/dist/paginate/index.js +6 -4
- package/dist/parity/index.d.ts +4 -5
- package/dist/parity/index.js +3 -2
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/index.d.ts +2 -2
- package/dist/pbt/index.js +2 -1
- package/dist/pbt/index.js.map +1 -1
- package/dist/persistence/index.d.ts +10 -12
- package/dist/persistence/index.js +5 -3
- package/dist/promise/index.d.ts +186 -0
- package/dist/promise/index.js +5 -0
- package/dist/promise/index.js.map +1 -0
- package/dist/{protocol-D8VbH-VH.d.ts → protocol-ClRZBhHQ.d.ts} +7 -8
- package/dist/react/index.d.ts +36 -17
- package/dist/react/index.js +27 -4
- package/dist/react/index.js.map +1 -1
- package/dist/resilience/index.d.ts +63 -456
- package/dist/resilience/index.js +37 -483
- package/dist/resilience/index.js.map +1 -1
- package/dist/{runtime-types-CMF13BBh.d.ts → runtime-types-B62Ucnxr.d.ts} +132 -389
- package/dist/testing/index.d.ts +33 -113
- package/dist/testing/index.js +39 -181
- package/dist/testing/index.js.map +1 -1
- package/dist/timing/index.d.ts +25 -22
- package/dist/timing/index.js +4 -3
- package/dist/timing/index.js.map +1 -1
- package/dist/{types-BseNPIhr.d.ts → types-DPm5YCDQ.d.ts} +2 -2
- package/dist/work-queue/index.d.ts +5 -6
- package/package.json +20 -7
- package/dist/chunk-22POWGFA.js.map +0 -1
- package/dist/chunk-3B5X7SBB.js.map +0 -1
- package/dist/chunk-66PY5WYF.js +0 -1816
- package/dist/chunk-66PY5WYF.js.map +0 -1
- package/dist/chunk-AYS5N6WY.js.map +0 -1
- package/dist/chunk-L3TND7LA.js +0 -97
- package/dist/chunk-L3TND7LA.js.map +0 -1
- package/dist/chunk-MYXFNWAJ.js.map +0 -1
- package/dist/chunk-Q5CQTBFA.js.map +0 -1
- package/dist/chunk-QPEVCJ5Q.js.map +0 -1
- package/dist/chunk-TCLBCERH.js.map +0 -1
- package/dist/chunk-VTQI2NHP.js.map +0 -1
- package/dist/index-ZCMhnVKN.d.ts +0 -272
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,111 +1,6 @@
|
|
|
1
|
-
import { C as Cmd, S as Sub, M as Machine,
|
|
2
|
-
import { C as CtxArg, R as Runtime } from '../runtime-types-
|
|
3
|
-
import '
|
|
4
|
-
import 'better-result';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The composed Model shape every `withX` wrapper produces: the base machine's
|
|
8
|
-
* state nested under `base`, and the wrapper's own NAMED, serializable slice
|
|
9
|
-
* under a single `$`-prefixed key. This helper does not care WHICH `$`-key the
|
|
10
|
-
* wrapper uses — it discovers it structurally (the one non-`base` field).
|
|
11
|
-
*/
|
|
12
|
-
type WrapperModel<S> = {
|
|
13
|
-
readonly base: S;
|
|
14
|
-
} & {
|
|
15
|
-
readonly [K: `$${string}`]: unknown;
|
|
16
|
-
};
|
|
17
|
-
/** Options for the conformance replay. Mirrors `replay`'s second argument. */
|
|
18
|
-
interface AssertWrapperFaithfulOpts<S, WM, BaseM, BaseCtx, WrapperCtx> {
|
|
19
|
-
/** The Msg sequence to fold through BOTH machines. Use the base's Msgs. */
|
|
20
|
-
readonly msgs: readonly BaseM[];
|
|
21
|
-
/** Ctx for the WRAPPED machine (base ctx + wrapper ports). */
|
|
22
|
-
readonly ctx: WrapperCtx;
|
|
23
|
-
/** Ctx for the BARE base machine. Defaults to `ctx` when omitted. */
|
|
24
|
-
readonly baseCtx?: BaseCtx;
|
|
25
|
-
/** Optional pre-`init` snapshot of the COMPOSED Model (wrapped rehydrate). */
|
|
26
|
-
readonly loaded?: WM | null;
|
|
27
|
-
/**
|
|
28
|
-
* Optional pre-`init` snapshot of the BARE base state. Supply when the
|
|
29
|
-
* wrapped `loaded` is non-null so the base-equivalence check rehydrates the
|
|
30
|
-
* base machine from the matching nested slice. Defaults to `loaded?.base`.
|
|
31
|
-
*/
|
|
32
|
-
readonly baseLoaded?: S | null;
|
|
33
|
-
/**
|
|
34
|
-
* Opt-in relaxation for an INTERCEPTING wrapper (e.g. `withResilience`).
|
|
35
|
-
*
|
|
36
|
-
* DEFAULT (`undefined` / `false`) — the OBSERVE-ONLY contract that
|
|
37
|
-
* `withTelemetry` and `withDeadline` satisfy: every bare base Cmd must appear
|
|
38
|
-
* UNCHANGED, in order, as a subsequence of the wrapped Cmd stream (properties
|
|
39
|
-
* 1 and 3b). The wrapper may APPEND its own Cmds but may NOT alter, drop, or
|
|
40
|
-
* retime a base Cmd. This is the strict default and it is NOT weakened.
|
|
41
|
-
*
|
|
42
|
-
* WHEN SET — for a wrapper whose whole job is to INTERCEPT a chosen base Cmd
|
|
43
|
-
* (retag it into a wrapper-owned carrier so the concern — cache / breaker /
|
|
44
|
-
* rate-limit / retry — can gate it), a target base Cmd LEGITIMATELY does not
|
|
45
|
-
* appear raw at the top level of the wrapped stream. The contract is still
|
|
46
|
-
* **retag-and-re-emit, never swallow** (rule 2): the Cmd must be RECORDED, not
|
|
47
|
-
* vanished. So when a bare base Cmd is ABSENT from the top-level wrapped
|
|
48
|
-
* subsequence, this option accepts it IFF, in that SAME transition window, it
|
|
49
|
-
* is recorded by the wrapper as one of:
|
|
50
|
-
*
|
|
51
|
-
* - the inner payload of a `$x:*` "run" carrier Cmd (`carrierCmdType` +
|
|
52
|
-
* `innerField`) — the retag path; OR
|
|
53
|
-
* - any other `$x:*` divergence Cmd whose `recordsField` deep-equals the
|
|
54
|
-
* base Cmd — the gate/defer path (e.g. a "fast-failed" or "deferred"
|
|
55
|
-
* record Cmd that carries the base Cmd it diverged on).
|
|
56
|
-
*
|
|
57
|
-
* A target base Cmd that is NEITHER present raw NOR recorded by any `$x:*`
|
|
58
|
-
* Cmd in its window is a SWALLOW betrayal and still fails the gate.
|
|
59
|
-
*
|
|
60
|
-
* Provide the field names the carrier uses so the gate can unwrap it
|
|
61
|
-
* structurally without hard-coding `withResilience`'s vocabulary.
|
|
62
|
-
*/
|
|
63
|
-
readonly intercepting?: InterceptingOpt;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Configures the intercepting relaxation (see
|
|
67
|
-
* `AssertWrapperFaithfulOpts.intercepting`). All fields name the carrier /
|
|
68
|
-
* divergence shape so the gate can structurally find a retagged base Cmd
|
|
69
|
-
* inside a `$x:*` Cmd. The gate only EVER inspects `$x:*`-prefixed wrapped
|
|
70
|
-
* Cmds for a recorded base Cmd — it never relaxes the check for a base Cmd
|
|
71
|
-
* that appears under any non-wrapper Cmd type.
|
|
72
|
-
*/
|
|
73
|
-
interface InterceptingOpt {
|
|
74
|
-
/**
|
|
75
|
-
* The `Cmd.type` of the wrapper's run-carrier (e.g. `"$resilience:run"`).
|
|
76
|
-
* A bare base Cmd that is retagged appears as `carrier[innerField]`.
|
|
77
|
-
*/
|
|
78
|
-
readonly carrierCmdType: string;
|
|
79
|
-
/** The field on the carrier Cmd that holds the original base Cmd. */
|
|
80
|
-
readonly innerField: string;
|
|
81
|
-
/**
|
|
82
|
-
* OPTIONAL field on ANY OTHER `$x:*` Cmd that records a base Cmd the wrapper
|
|
83
|
-
* gated/deferred without running it (e.g. a fast-fail record). When present
|
|
84
|
-
* on a `$x:*` Cmd and deep-equal to the missing base Cmd, it counts as a
|
|
85
|
-
* recorded divergence. Omit when the wrapper only ever retags via the
|
|
86
|
-
* carrier.
|
|
87
|
-
*/
|
|
88
|
-
readonly recordsField?: string;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Assert a `withX` wrapper is TEA-faithful. Replays the wrapped machine and the
|
|
92
|
-
* bare base machine over the same Msgs and checks the four conformance
|
|
93
|
-
* properties (see file header).
|
|
94
|
-
*
|
|
95
|
-
* @param base the bare base `Machine`.
|
|
96
|
-
* @param makeWrapped a thunk that wraps `base` and returns the composed machine
|
|
97
|
-
* (e.g. `() => withTelemetry(base, cfg)`). A thunk — not the wrapped value —
|
|
98
|
-
* so a wrapper that registers process-scoped ports (`definePort`) is
|
|
99
|
-
* constructed inside the assertion's control, and so the same betrayal-
|
|
100
|
-
* detector reads as `assertWrapperFaithful(base, () => withX(base, cfg), ...)`
|
|
101
|
-
* at every wrapper's test site.
|
|
102
|
-
* @param opts the conformance replay options.
|
|
103
|
-
*/
|
|
104
|
-
declare function assertWrapperFaithful<S, BaseM extends {
|
|
105
|
-
type: string;
|
|
106
|
-
}, BaseC extends Cmd, BaseU extends Sub, BaseCtx, WM extends WrapperModel<S>, WrapperM extends {
|
|
107
|
-
type: string;
|
|
108
|
-
}, WrapperC extends Cmd, WrapperU extends Sub, WrapperCtx>(base: Machine<S, BaseM, BaseC, BaseU, BaseCtx>, makeWrapped: () => Machine<WM, WrapperM, WrapperC, WrapperU, WrapperCtx>, opts: AssertWrapperFaithfulOpts<S, WM, BaseM, BaseCtx, WrapperCtx>): void;
|
|
1
|
+
import { C as Cmd, S as Sub, M as Machine, a as BuiltinSub, o as Interpret } from '../core-rDbOaJTN.js';
|
|
2
|
+
import { C as CtxArg, R as Runtime } from '../runtime-types-B62Ucnxr.js';
|
|
3
|
+
import '@standard-schema/spec';
|
|
109
4
|
|
|
110
5
|
/**
|
|
111
6
|
* Shared options shape for every test assertion below. Mirrors the
|
|
@@ -239,7 +134,7 @@ interface BoundMachine<S, M extends {
|
|
|
239
134
|
* Assert the exact set of subs desired at the final state. Mirrors the
|
|
240
135
|
* free `expectActiveSubs(machine, opts, expected)`.
|
|
241
136
|
*/
|
|
242
|
-
expectActiveSubs(opts: BoundOpts<S, M>, expected: readonly NoInfer<U
|
|
137
|
+
expectActiveSubs(opts: BoundOpts<S, M>, expected: readonly NoInfer<U | BuiltinSub<M>>[]): void;
|
|
243
138
|
/**
|
|
244
139
|
* Bound `replay` — returns `{ state, cmds, subs }` for the given opts.
|
|
245
140
|
* Use for the "narrow-then-assert" pattern where the test inspects a
|
|
@@ -248,7 +143,7 @@ interface BoundMachine<S, M extends {
|
|
|
248
143
|
replay(opts: BoundOpts<S, M>): {
|
|
249
144
|
state: S;
|
|
250
145
|
cmds: readonly C[];
|
|
251
|
-
subs: readonly U[];
|
|
146
|
+
subs: readonly (U | BuiltinSub<M>)[];
|
|
252
147
|
};
|
|
253
148
|
}
|
|
254
149
|
/**
|
|
@@ -324,6 +219,12 @@ type DriveOptions<Ctx> = DriveCtxArg<Ctx> & {
|
|
|
324
219
|
* {@link DEFAULT_MAX_ROUNDS} (100).
|
|
325
220
|
*/
|
|
326
221
|
readonly maxRounds?: number;
|
|
222
|
+
/**
|
|
223
|
+
* The clock that stamps `at` on a `Cmd.define`d Cmd's minted `_ok` / `_err`
|
|
224
|
+
* Msg — the one `run` takes. Defaults to `Date.now`; pin it for a test that
|
|
225
|
+
* asserts on `at`.
|
|
226
|
+
*/
|
|
227
|
+
readonly clock?: () => number;
|
|
327
228
|
};
|
|
328
229
|
/**
|
|
329
230
|
* Raised when a driven machine is still emitting work after `maxRounds` rounds.
|
|
@@ -386,8 +287,8 @@ declare function driveTraceOf<M, C>(err: unknown): readonly DriveTraceEntry<M, C
|
|
|
386
287
|
* `handlers`, feeding every settle Msg back until the machine goes quiet, and
|
|
387
288
|
* hand back the settled state together with the whole history.
|
|
388
289
|
*
|
|
389
|
-
* `handlers` is the same
|
|
390
|
-
*
|
|
290
|
+
* `handlers` is the same table a host hands `run` as its `interpret` (e.g.
|
|
291
|
+
* a jev knob's `resilient_run` handler), so the test drives the real interpreter and
|
|
391
292
|
* mocks only the port beneath it.
|
|
392
293
|
*
|
|
393
294
|
* One round is: fold every pending Msg, then await a handler for every Cmd
|
|
@@ -409,6 +310,25 @@ declare function drive<S, M extends {
|
|
|
409
310
|
type: string;
|
|
410
311
|
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, initial: S, msg: M, handlers: Interpret<M, C, Ctx>, ...[opts]: Record<never, never> extends DriveOptions<Ctx> ? [opts?: DriveOptions<Ctx>] : [opts: DriveOptions<Ctx>]): Promise<DriveResult<S, M, C>>;
|
|
411
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Assert that replaying `opts.msgs` is a pure function of the Msg log: the
|
|
315
|
+
* final state and every emitted Cmd come out the same under two different
|
|
316
|
+
* global wall-clocks and RNG seeds.
|
|
317
|
+
*
|
|
318
|
+
* `init` and `update` never receive a clock, so a cell that reads
|
|
319
|
+
* `Date.now()` or `Math.random()` reaches for the global one, and a replay of
|
|
320
|
+
* the same log on another day or another host lands somewhere else. Two
|
|
321
|
+
* back-to-back replays under one clock would hide that; this helper stubs
|
|
322
|
+
* two distinct ambients and compares. Time belongs in the Msg (an `at`
|
|
323
|
+
* field), where it is identical across replays.
|
|
324
|
+
*
|
|
325
|
+
* Subs are not compared: they are derived from the final state, so a state
|
|
326
|
+
* that matches yields the same Subs.
|
|
327
|
+
*/
|
|
328
|
+
declare function expectReplayDeterministic<S, M extends {
|
|
329
|
+
type: string;
|
|
330
|
+
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, opts: NoInfer<ReplayOpts<S, M, Ctx>>): void;
|
|
331
|
+
|
|
412
332
|
/**
|
|
413
333
|
* Construct an inert `Runtime<S, M>` value. Use at ctx sites that require
|
|
414
334
|
* a Runtime reference but where the test never invokes `interpret` (so
|
|
@@ -468,4 +388,4 @@ declare function stateFactory<S extends {
|
|
|
468
388
|
type: string;
|
|
469
389
|
}>(defaults: StateFactoryDefaults<S>): StateFactoryAPI<S>;
|
|
470
390
|
|
|
471
|
-
export { type
|
|
391
|
+
export { type BoundMachine, DEFAULT_MAX_ROUNDS, type DriveCtxArg, DriveNoHandlerError, type DriveOptions, type DriveResult, DriveRoundsExceededError, type DriveTraceEntry, type ReplayOpts, type StateFactoryAPI, type StateFactoryDefaults, bindMachine, drive, driveTraceOf, expectActiveSubs, expectCmdEmitted, expectCmdSequence, expectFinalState, expectReplayDeterministic, noopRuntime, stateFactory, step };
|
package/dist/testing/index.js
CHANGED
|
@@ -1,180 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '../chunk-C4U5D5HP.js';
|
|
2
|
+
import { replay, cmdEdgeOver, Outcome, cmdEdge } from '../chunk-K4ETBNKS.js';
|
|
2
3
|
import { expect, vi } from 'vitest';
|
|
3
4
|
|
|
4
|
-
function assertWrapperFaithful(base, makeWrapped, opts) {
|
|
5
|
-
const baseCtx = opts.baseCtx ?? opts.ctx;
|
|
6
|
-
const wrappedMsgs = opts.msgs;
|
|
7
|
-
const baseLoaded = opts.baseLoaded ?? (opts.loaded != null ? opts.loaded.base : null);
|
|
8
|
-
const A0 = { now: 1e6, rng: 0.123 };
|
|
9
|
-
const A1 = { now: 9876543, rng: 0.987 };
|
|
10
|
-
const bare = underAmbient(
|
|
11
|
-
A0,
|
|
12
|
-
() => replay(base, { msgs: opts.msgs, ctx: baseCtx, loaded: baseLoaded })
|
|
13
|
-
);
|
|
14
|
-
const wrapped = underAmbient(A0, () => makeWrapped());
|
|
15
|
-
const wrappedA = underAmbient(
|
|
16
|
-
A0,
|
|
17
|
-
() => replay(wrapped, {
|
|
18
|
-
msgs: wrappedMsgs,
|
|
19
|
-
ctx: opts.ctx,
|
|
20
|
-
loaded: opts.loaded ?? null
|
|
21
|
-
})
|
|
22
|
-
);
|
|
23
|
-
const wrappedB = underAmbient(
|
|
24
|
-
A1,
|
|
25
|
-
() => replay(makeWrapped(), {
|
|
26
|
-
msgs: wrappedMsgs,
|
|
27
|
-
ctx: opts.ctx,
|
|
28
|
-
loaded: opts.loaded ?? null
|
|
29
|
-
})
|
|
30
|
-
);
|
|
31
|
-
expect(wrappedA.state.base).toEqual(bare.state);
|
|
32
|
-
expectCmdSubsequence(bare.cmds, wrappedA.cmds, void 0, opts.intercepting);
|
|
33
|
-
const sliceKey = wrapperSliceKey(wrappedA.state);
|
|
34
|
-
expect(
|
|
35
|
-
sliceOf(wrappedB.state, sliceKey),
|
|
36
|
-
`wrapper slice changed under a different ambient clock/RNG \u2192 update is reading Date.now()/Math.random(); time must enter only as Msg \`at\` data`
|
|
37
|
-
).toEqual(sliceOf(wrappedA.state, sliceKey));
|
|
38
|
-
const wrapperFamily = `${sliceKey}:`;
|
|
39
|
-
const wrapperCmdsA = wrappedA.cmds.filter(
|
|
40
|
-
(c) => c.type.startsWith(wrapperFamily)
|
|
41
|
-
);
|
|
42
|
-
const wrapperCmdsB = wrappedB.cmds.filter(
|
|
43
|
-
(c) => c.type.startsWith(wrapperFamily)
|
|
44
|
-
);
|
|
45
|
-
expect(
|
|
46
|
-
wrapperCmdsB,
|
|
47
|
-
`wrapper Cmds ("${wrapperFamily}*") changed under a different ambient clock/RNG \u2192 a wrapper Cmd PAYLOAD is reading Date.now()/Math.random(); wrapper Cmds must be a pure function of the Msg log, time entering only as Msg \`at\` data`
|
|
48
|
-
).toEqual(wrapperCmdsA);
|
|
49
|
-
if (opts.msgs.length > 0) {
|
|
50
|
-
expect(
|
|
51
|
-
wrapperCmdsA.length,
|
|
52
|
-
`expected at least one "${wrapperFamily}*" wrapper-decision Cmd in the replay log (found none across ${opts.msgs.length} transition(s)) \u2014 a wrapper whose decisions are invisible to the Msg/Cmd log has betrayed total visibility`
|
|
53
|
-
).toBeGreaterThan(0);
|
|
54
|
-
}
|
|
55
|
-
for (let i = 0; i < opts.msgs.length; i++) {
|
|
56
|
-
const bareStep = stepCmds(
|
|
57
|
-
() => underAmbient(
|
|
58
|
-
A0,
|
|
59
|
-
() => replay(base, {
|
|
60
|
-
msgs: opts.msgs.slice(0, i),
|
|
61
|
-
ctx: baseCtx,
|
|
62
|
-
loaded: baseLoaded
|
|
63
|
-
})
|
|
64
|
-
).cmds,
|
|
65
|
-
() => underAmbient(
|
|
66
|
-
A0,
|
|
67
|
-
() => replay(base, {
|
|
68
|
-
msgs: opts.msgs.slice(0, i + 1),
|
|
69
|
-
ctx: baseCtx,
|
|
70
|
-
loaded: baseLoaded
|
|
71
|
-
})
|
|
72
|
-
).cmds
|
|
73
|
-
);
|
|
74
|
-
const wrappedStep = stepCmds(
|
|
75
|
-
() => underAmbient(
|
|
76
|
-
A0,
|
|
77
|
-
() => replay(wrapped, {
|
|
78
|
-
msgs: wrappedMsgs.slice(0, i),
|
|
79
|
-
ctx: opts.ctx,
|
|
80
|
-
loaded: opts.loaded ?? null
|
|
81
|
-
})
|
|
82
|
-
).cmds,
|
|
83
|
-
() => underAmbient(
|
|
84
|
-
A0,
|
|
85
|
-
() => replay(wrapped, {
|
|
86
|
-
msgs: wrappedMsgs.slice(0, i + 1),
|
|
87
|
-
ctx: opts.ctx,
|
|
88
|
-
loaded: opts.loaded ?? null
|
|
89
|
-
})
|
|
90
|
-
).cmds
|
|
91
|
-
);
|
|
92
|
-
expectCmdSubsequence(bareStep, wrappedStep, i, opts.intercepting);
|
|
93
|
-
}
|
|
94
|
-
const slice = sliceOf(wrappedA.state, sliceKey);
|
|
95
|
-
expect(JSON.parse(JSON.stringify(slice))).toEqual(slice);
|
|
96
|
-
}
|
|
97
|
-
function underAmbient(ambient, fn) {
|
|
98
|
-
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(ambient.now);
|
|
99
|
-
const rngSpy = vi.spyOn(Math, "random").mockReturnValue(ambient.rng);
|
|
100
|
-
try {
|
|
101
|
-
return fn();
|
|
102
|
-
} finally {
|
|
103
|
-
nowSpy.mockRestore();
|
|
104
|
-
rngSpy.mockRestore();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function sliceOf(model, sliceKey) {
|
|
108
|
-
return model[sliceKey];
|
|
109
|
-
}
|
|
110
|
-
function stepCmds(prefixCmds, throughCmds) {
|
|
111
|
-
const before = prefixCmds();
|
|
112
|
-
const after = throughCmds();
|
|
113
|
-
return after.slice(before.length);
|
|
114
|
-
}
|
|
115
|
-
function wrapperSliceKey(model) {
|
|
116
|
-
const dollarKeys = Object.keys(model).filter((k) => k.startsWith("$"));
|
|
117
|
-
if (dollarKeys.length !== 1) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`assertWrapperFaithful: expected exactly one "$"-prefixed wrapper slice on the composed Model, found ${dollarKeys.length} (${dollarKeys.join(", ") || "none"}). Every withX wrapper owns exactly one named slice (e.g. "$telemetry").`
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
return dollarKeys[0];
|
|
123
|
-
}
|
|
124
|
-
function expectCmdSubsequence(needle, haystack, step2, intercepting) {
|
|
125
|
-
let i = 0;
|
|
126
|
-
for (const cmd of haystack) {
|
|
127
|
-
if (i >= needle.length) break;
|
|
128
|
-
if (__deepEqual(needle[i], cmd)) {
|
|
129
|
-
i++;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (intercepting !== void 0 && recordsBaseCmd(cmd, needle[i], intercepting)) {
|
|
133
|
-
i++;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const where = step2 === void 0 ? "the wrapped Cmd stream" : `the wrapped Cmd stream AT TRANSITION ${step2} (a base Cmd deferred to a later transition is an intra-window retiming betrayal)`;
|
|
137
|
-
const swallowHint = intercepting === void 0 ? "a missing/rewritten/retimed base Cmd is a swallow betrayal" : `an intercepting wrapper must RETAG each target base Cmd into a "${intercepting.carrierCmdType}" carrier (inner field "${intercepting.innerField}") or record it on a "$*:" divergence Cmd \u2014 a base Cmd neither present raw NOR recorded by any "$*:" Cmd is a swallow betrayal`;
|
|
138
|
-
expect(
|
|
139
|
-
i,
|
|
140
|
-
`expected every base Cmd to pass through unchanged (matched ${i} of ${needle.length} base Cmds as an ordered subsequence of ${where}) \u2014 ${swallowHint}`
|
|
141
|
-
).toBe(needle.length);
|
|
142
|
-
}
|
|
143
|
-
function recordsBaseCmd(cmd, baseCmd, opt) {
|
|
144
|
-
if (!cmd.type.startsWith("$")) return false;
|
|
145
|
-
const rec = cmd;
|
|
146
|
-
if (cmd.type === opt.carrierCmdType && __deepEqual(rec[opt.innerField], baseCmd))
|
|
147
|
-
return true;
|
|
148
|
-
if (opt.recordsField !== void 0 && __deepEqual(rec[opt.recordsField], baseCmd))
|
|
149
|
-
return true;
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
function __deepEqual(a, b) {
|
|
153
|
-
if (Object.is(a, b)) return true;
|
|
154
|
-
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
const aIsArray = Array.isArray(a);
|
|
158
|
-
if (aIsArray !== Array.isArray(b)) return false;
|
|
159
|
-
if (aIsArray) {
|
|
160
|
-
const bArr = b;
|
|
161
|
-
if (a.length !== bArr.length) return false;
|
|
162
|
-
for (let i = 0; i < a.length; i++) {
|
|
163
|
-
if (!__deepEqual(a[i], bArr[i])) return false;
|
|
164
|
-
}
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
const aRec = a;
|
|
168
|
-
const bRec = b;
|
|
169
|
-
const aKeys = Object.keys(aRec);
|
|
170
|
-
const bKeys = Object.keys(bRec);
|
|
171
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
172
|
-
for (const key of aKeys) {
|
|
173
|
-
if (!Object.hasOwn(bRec, key)) return false;
|
|
174
|
-
if (!__deepEqual(aRec[key], bRec[key])) return false;
|
|
175
|
-
}
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
5
|
function expectFinalState(machine, opts, expected) {
|
|
179
6
|
const { state } = replay(machine, opts);
|
|
180
7
|
expect(state).toEqual(expected);
|
|
@@ -263,10 +90,14 @@ async function drive(machine, initial, msg, handlers, ...[opts]) {
|
|
|
263
90
|
const options = opts ?? {};
|
|
264
91
|
const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
265
92
|
const bound = bindMachine(machine, options.ctx);
|
|
93
|
+
const settle = cmdEdgeOver(machine.cmds ?? [], options.clock ?? Date.now);
|
|
266
94
|
const noopEmit = { emit: () => {
|
|
267
95
|
} };
|
|
268
96
|
const handlerCtx = {
|
|
269
97
|
...noopEmit,
|
|
98
|
+
ok: Outcome.ok,
|
|
99
|
+
err: Outcome.err,
|
|
100
|
+
[cmdEdge]: settle,
|
|
270
101
|
...options.ctx
|
|
271
102
|
};
|
|
272
103
|
const cells = handlers;
|
|
@@ -294,11 +125,14 @@ async function drive(machine, initial, msg, handlers, ...[opts]) {
|
|
|
294
125
|
if (cell === void 0) {
|
|
295
126
|
throw new DriveNoHandlerError(cmd.type, trace);
|
|
296
127
|
}
|
|
297
|
-
let
|
|
128
|
+
let settled;
|
|
298
129
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
130
|
+
settled = settle(
|
|
131
|
+
cmd,
|
|
132
|
+
await cell(cmd, handlerCtx, (fired) => {
|
|
133
|
+
pending.push(fired);
|
|
134
|
+
})
|
|
135
|
+
);
|
|
302
136
|
} catch (err) {
|
|
303
137
|
if (typeof err === "object" && err !== null) {
|
|
304
138
|
Object.defineProperty(err, TRACE_KEY, {
|
|
@@ -310,11 +144,35 @@ async function drive(machine, initial, msg, handlers, ...[opts]) {
|
|
|
310
144
|
}
|
|
311
145
|
throw err;
|
|
312
146
|
}
|
|
313
|
-
if (
|
|
147
|
+
if (settled !== void 0) pending.push(settled);
|
|
314
148
|
}
|
|
315
149
|
}
|
|
316
150
|
return { state, trace };
|
|
317
151
|
}
|
|
152
|
+
var AMBIENT_A = { now: 1e6, rng: 0.123 };
|
|
153
|
+
var AMBIENT_B = { now: 9876543, rng: 0.987 };
|
|
154
|
+
function expectReplayDeterministic(machine, opts) {
|
|
155
|
+
const a = underAmbient(AMBIENT_A, () => replay(machine, opts));
|
|
156
|
+
const b = underAmbient(AMBIENT_B, () => replay(machine, opts));
|
|
157
|
+
expect(
|
|
158
|
+
b.state,
|
|
159
|
+
"final state changed under a different clock/RNG: init or update reads Date.now()/Math.random(); time must enter as Msg data"
|
|
160
|
+
).toEqual(a.state);
|
|
161
|
+
expect(
|
|
162
|
+
b.cmds,
|
|
163
|
+
"emitted Cmds changed under a different clock/RNG: a Cmd payload reads Date.now()/Math.random(); time must enter as Msg data"
|
|
164
|
+
).toEqual(a.cmds);
|
|
165
|
+
}
|
|
166
|
+
function underAmbient(ambient, fn) {
|
|
167
|
+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(ambient.now);
|
|
168
|
+
const rngSpy = vi.spyOn(Math, "random").mockReturnValue(ambient.rng);
|
|
169
|
+
try {
|
|
170
|
+
return fn();
|
|
171
|
+
} finally {
|
|
172
|
+
nowSpy.mockRestore();
|
|
173
|
+
rngSpy.mockRestore();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
318
176
|
|
|
319
177
|
// src/testing/noop-runtime.ts
|
|
320
178
|
function noopRuntime(opts) {
|
|
@@ -390,6 +248,6 @@ function stateFactory(defaults) {
|
|
|
390
248
|
return api;
|
|
391
249
|
}
|
|
392
250
|
|
|
393
|
-
export { DEFAULT_MAX_ROUNDS, DriveNoHandlerError, DriveRoundsExceededError,
|
|
251
|
+
export { DEFAULT_MAX_ROUNDS, DriveNoHandlerError, DriveRoundsExceededError, bindMachine, drive, driveTraceOf, expectActiveSubs, expectCmdEmitted, expectCmdSequence, expectFinalState, expectReplayDeterministic, noopRuntime, stateFactory, step };
|
|
394
252
|
//# sourceMappingURL=index.js.map
|
|
395
253
|
//# sourceMappingURL=index.js.map
|