@kairos-es/testing 0.0.0 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -4
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/specification.js +37 -11
- package/dist/cjs/specification.js.map +1 -1
- package/dist/dts/index.d.ts +4 -3
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/specification.d.ts +53 -7
- package/dist/dts/specification.d.ts.map +1 -1
- package/dist/esm/index.js +4 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/specification.js +35 -10
- package/dist/esm/specification.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +4 -3
- package/src/specification.ts +92 -26
package/README.md
CHANGED
|
@@ -2,10 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
Given/When/Then decision-model testing surface for kairos-es.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
A **development** dependency: it peers `@effect/vitest` and is imported from
|
|
8
|
+
tests only.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add -D @kairos-es/testing@alpha @effect/vitest vitest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`vitest` is named because `@effect/vitest` requires it and pnpm does not install
|
|
15
|
+
a peer for you.
|
|
16
|
+
|
|
17
|
+
Its other peers, `@kairos-es/core`, `@kairos-es/codec` and `effect`, are the ones
|
|
18
|
+
an application using the library already has.
|
|
19
|
+
|
|
20
|
+
A `DeciderSpecification`-equivalent running the real read → decode → decide →
|
|
21
|
+
append cycle against the in-memory store rather than a mock, on
|
|
22
|
+
`@effect/vitest`. Over the codec's `buildDecisionModel` for a single-hop
|
|
23
|
+
decision, and over `modelAtHead` and the read ledger for a layered one.
|
|
24
|
+
`@kairos-es/core`, `@kairos-es/codec`, `effect` and `@effect/vitest` are peer
|
|
25
|
+
dependencies.
|
|
26
|
+
|
|
27
|
+
`specificationFor(decider)` is the ordinary entry point.
|
|
28
|
+
`specificationForCycle(run)` is the general one it is a one-line specialisation
|
|
29
|
+
of: it takes any `(command) => Effect<Position, …>`, so a `transact` bracket slots
|
|
30
|
+
into the same chain a decider does. Its `run` states its errors as `E | CycleError`
|
|
31
|
+
— the chain adds that union itself, so a naive `Effect<Position, E, R>`
|
|
32
|
+
double-counts it and will not unify.
|
|
33
|
+
|
|
34
|
+
⚠ `.thenError(tag)` is weaker on a bracket than on a decider, in three ways worth
|
|
35
|
+
knowing before you rely on it. Two different denials from one guard usually share
|
|
36
|
+
one tag, so a spec can pass on a refusal its author did not mean. An
|
|
37
|
+
`AppendConditionFailed` now also fires for an EARLIER hop, so the tag no longer says
|
|
38
|
+
which boundary moved. And `NoReadsRecorded` reads like a domain assertion while
|
|
39
|
+
being a statement about wiring. A layered or permission suite should assert more
|
|
40
|
+
than the tag.
|
|
9
41
|
|
|
10
42
|
The `Decider` a scenario exercises and the `runDecider` that runs it are
|
|
11
43
|
production symbols and belong to [`@kairos-es/codec`](../codec); this package
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["_specification","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":["_specification","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;AA2BA,IAAAA,cAAA,GAAAC,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAH,cAAA,EAAAI,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAL,cAAA,CAAAK,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAT,cAAA,CAAAK,GAAA;IAAA;EAAA;AAAA","ignoreList":[]}
|
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.specificationFor = exports.scenario = void 0;
|
|
6
|
+
exports.specificationForCycle = exports.specificationFor = exports.scenario = void 0;
|
|
7
7
|
var _vitest = require("@effect/vitest");
|
|
8
8
|
var _codec = require("@kairos-es/codec");
|
|
9
9
|
var _core = require("@kairos-es/core");
|
|
10
10
|
var _effect = require("effect");
|
|
11
11
|
/**
|
|
12
|
-
* The Given/When/Then surface: a `DeciderSpecification`-equivalent over
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The Given/When/Then surface: a `DeciderSpecification`-equivalent over a real
|
|
13
|
+
* fold and a `DcbEventStore` (the in-memory layer in practice) —
|
|
14
|
+
* `@kairos-es/codec`'s `buildDecisionModel` for a single-hop decision, and
|
|
15
|
+
* `modelAtHead` plus the read ledger for a layered one. `specificationForCycle`
|
|
16
|
+
* is the general entry point over either, named for the command CYCLE it takes
|
|
17
|
+
* rather than for the layered bracket that was its first caller — the shape it
|
|
18
|
+
* accepts is `(command) => Effect<Position, …>` and a decider is one instance of
|
|
19
|
+
* it. It is shaped so an Event Modeling
|
|
15
20
|
* `Specification` (given events, when a command, then events / thenError a tag)
|
|
16
21
|
* compiles onto it 1:1 — prior art: dcb-events `TestRunner` and Emmett's
|
|
17
22
|
* `DeciderSpecification`.
|
|
@@ -91,21 +96,31 @@ const appendedAfter = after => _effect.Effect.gen(function* () {
|
|
|
91
96
|
return yield* collect(events);
|
|
92
97
|
});
|
|
93
98
|
/**
|
|
94
|
-
* Begin a specification for
|
|
95
|
-
*
|
|
96
|
-
*
|
|
99
|
+
* Begin a specification for ANY command cycle that returns the appended
|
|
100
|
+
* `Position` — a bracket for one command as readily as a decider.
|
|
101
|
+
*
|
|
102
|
+
* This is the general form and `specificationFor` below is a one-line
|
|
103
|
+
* specialisation of it, which is the result worth stating rather than the
|
|
104
|
+
* mechanism: a `Decider` is one particular `(command) => Effect<Position, …>`,
|
|
105
|
+
* and `runDecider` is the function that says so. Nothing downstream of `when`
|
|
106
|
+
* moves — the same seed boundary, the same read-back, the same shape comparison
|
|
107
|
+
* — because both cycles already agree on what they return.
|
|
108
|
+
*
|
|
109
|
+
* ⚠ `run`'s error channel is written `E | CycleError` and must be: the chain adds
|
|
110
|
+
* that union itself, so a naive `Effect<Position, E, R>` double-counts it and
|
|
111
|
+
* fails to unify. `CycleError` exists to be written here.
|
|
97
112
|
*/
|
|
98
|
-
const
|
|
113
|
+
const specificationForCycle = run => ({
|
|
99
114
|
given: (...pastEvents) => ({
|
|
100
115
|
when: command => {
|
|
101
|
-
// Run the seed +
|
|
116
|
+
// Run the seed + cycle, exposing the outcome as an Exit so both
|
|
102
117
|
// branches (then / thenError) can inspect success or failure uniformly. The
|
|
103
118
|
// seed is inside the Exit too, so a decode failure while materialising a
|
|
104
119
|
// given producer is captured as a failure rather than escaping the channel.
|
|
105
120
|
// The success value pairs the seed boundary (the last seeded position) with
|
|
106
|
-
// the
|
|
121
|
+
// the cycle's outcome, so `then` reads strictly after that boundary
|
|
107
122
|
// rather than dropping a fixed row count (correct on a gappy store).
|
|
108
|
-
const runToExit = seed(pastEvents).pipe(_effect.Effect.zip((
|
|
123
|
+
const runToExit = seed(pastEvents).pipe(_effect.Effect.zip(run(command)), _effect.Effect.exit);
|
|
109
124
|
return {
|
|
110
125
|
// biome-ignore lint/suspicious/noThenProperty: deliberate Given/When/Then terminal name (see WhenSpec.then); never awaited as a thenable.
|
|
111
126
|
then: (...expected) => _effect.Effect.gen(function* () {
|
|
@@ -135,6 +150,17 @@ const specificationFor = decider => ({
|
|
|
135
150
|
}
|
|
136
151
|
})
|
|
137
152
|
});
|
|
153
|
+
/**
|
|
154
|
+
* Begin a specification for `decider`. The returned `given(...)` starts the
|
|
155
|
+
* Given/When/Then chain; the terminal step yields an `Effect` requiring
|
|
156
|
+
* `DcbEventStore | Serializer | Uuid | R | RequirementOf<P>`.
|
|
157
|
+
*
|
|
158
|
+
* One line over the layered sibling above, and deliberately not a twin of it:
|
|
159
|
+
* a duplicated body would be two sets of tests and two things free to drift on
|
|
160
|
+
* the seed boundary, the read-back or the shape comparison.
|
|
161
|
+
*/
|
|
162
|
+
exports.specificationForCycle = specificationForCycle;
|
|
163
|
+
const specificationFor = decider => specificationForCycle(command => (0, _codec.runDecider)(decider, command));
|
|
138
164
|
/**
|
|
139
165
|
* `scenario(description, spec => spec.given(...).when(...).then(...))` — register
|
|
140
166
|
* an `@effect/vitest` `it.effect` case running `build` against a fresh in-memory
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specification.js","names":["_vitest","require","_codec","_core","_effect","shapeOf","event","type","tags","data","collect","events","Stream","runCollect","pipe","Effect","map","Chunk","toReadonlyArray","seed","producers","gen","store","DcbEventStore","last","ORIGIN","producer","append","orDie","appendedAfter","after","read","Q","all","
|
|
1
|
+
{"version":3,"file":"specification.js","names":["_vitest","require","_codec","_core","_effect","shapeOf","event","type","tags","data","collect","events","Stream","runCollect","pipe","Effect","map","Chunk","toReadonlyArray","seed","producers","gen","store","DcbEventStore","last","ORIGIN","producer","append","orDie","appendedAfter","after","read","Q","all","specificationForCycle","run","given","pastEvents","when","command","runToExit","zip","exit","then","expected","Exit","isFailure","failCause","cause","seedBoundary","value","appended","expectedEvents","expect","s","toEqual","thenError","tag","toBe","failure","Cause","failureOption","error","_tag","undefined","actualTag","exports","specificationFor","decider","runDecider","scenario","description","build","it","effect","provide","Layer","mergeAll","fresh","DcbEventStoreInMemory","SerializerDefault","UuidTest"],"sources":["../../src/specification.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
package/dist/dts/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @kairos-es/testing — the Given/When/Then decision-model testing surface.
|
|
3
3
|
*
|
|
4
|
-
* A `DeciderSpecification`-equivalent over
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A `DeciderSpecification`-equivalent over a real fold and a `DcbEventStore` (in
|
|
5
|
+
* practice the in-memory layer): `@kairos-es/codec`'s `buildDecisionModel` for
|
|
6
|
+
* a single-hop decision, and `modelAtHead` plus the read ledger for a layered
|
|
7
|
+
* one. Shaped so an Event Modeling `Specification` (given events / when a
|
|
7
8
|
* command / then events / thenError a tag) compiles onto it 1:1.
|
|
8
9
|
*
|
|
9
10
|
* scenario('subscribe a student', subscribeDecider, (spec) =>
|
package/dist/dts/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,cAAc,iBAAiB,CAAA"}
|
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
import type { CodecError, Decider, RequirementOf, SliceProjection } from '@kairos-es/codec';
|
|
1
|
+
import type { CodecError, Decider, NoReadsRecorded, RequirementOf, SliceProjection, TaglessSliceInLedger } from '@kairos-es/codec';
|
|
2
2
|
import { type Serializer, type Uuid } from '@kairos-es/codec';
|
|
3
|
-
import { type AppendConditionFailed, type DcbEvent, DcbEventStore } from '@kairos-es/core';
|
|
3
|
+
import { type AppendConditionFailed, type DcbEvent, DcbEventStore, type Position } from '@kairos-es/core';
|
|
4
4
|
import { Effect } from 'effect';
|
|
5
5
|
/** An effectful producer of a wire event (the `toEvent` result an app authors). */
|
|
6
6
|
export type EventProducer<R> = Effect.Effect<DcbEvent, CodecError, Serializer | Uuid | R>;
|
|
7
|
+
/**
|
|
8
|
+
* The errors a command CYCLE contributes, beyond whatever `E` the decision
|
|
9
|
+
* itself fails with.
|
|
10
|
+
*
|
|
11
|
+
* Named once and exported because it is what makes the layered entry point below
|
|
12
|
+
* UNIFY. `WhenSpec.then` adds this union itself, so a `run` written as a naive
|
|
13
|
+
* `Effect<Position, E, R>` double-counts every member of it and fails to
|
|
14
|
+
* typecheck against a chain that has already added them — a trap with an
|
|
15
|
+
* unhelpful error message, met once and closed here by giving the union a name a
|
|
16
|
+
* caller can write.
|
|
17
|
+
*
|
|
18
|
+
* `NoReadsRecorded` is reachable only on the layered path and `TaglessSliceInLedger`
|
|
19
|
+
* only where a bracket is open, but both are members here rather than in a second
|
|
20
|
+
* union, because the two paths share every step downstream of `when` and a second
|
|
21
|
+
* union would be a second thing to keep in step for no caller's benefit.
|
|
22
|
+
*/
|
|
23
|
+
export type CycleError = CodecError | AppendConditionFailed | TaglessSliceInLedger | NoReadsRecorded;
|
|
7
24
|
/**
|
|
8
25
|
* The entry point of the Given/When/Then chain: what `specificationFor` returns
|
|
9
26
|
* and what `scenario` hands its `build` callback. It is NAMED rather than
|
|
@@ -41,22 +58,51 @@ export interface WhenSpec<E, R, PR> {
|
|
|
41
58
|
* Assert the success branch: the decider appended exactly these events
|
|
42
59
|
* (compared structurally by type/tags/data, in order). The expected events are
|
|
43
60
|
* effectful producers, materialised under the provided layers. The error
|
|
44
|
-
* channel keeps `E |
|
|
45
|
-
*
|
|
46
|
-
*
|
|
61
|
+
* channel keeps `E | CycleError` because a genuinely failing cycle re-raises
|
|
62
|
+
* its cause (a `.then` on a failing decision is a test-authoring mistake, made
|
|
63
|
+
* loud rather than silently green).
|
|
47
64
|
*/
|
|
48
|
-
readonly then: (...expected: ReadonlyArray<EventProducer<R>>) => Effect.Effect<void, E |
|
|
65
|
+
readonly then: (...expected: ReadonlyArray<EventProducer<R>>) => Effect.Effect<void, E | CycleError, DcbEventStore | Serializer | Uuid | R | PR>;
|
|
49
66
|
/**
|
|
50
67
|
* Assert the failure branch: the cycle failed with a tagged error whose
|
|
51
68
|
* `_tag` equals `tag` (a domain error from `decide`, a `CodecError` from
|
|
52
69
|
* decode, or `AppendConditionFailed` from a moved consistency boundary).
|
|
70
|
+
*
|
|
71
|
+
* ⚠ THREE WAYS THIS PASSES FOR THE WRONG REASON, and a layered or permission
|
|
72
|
+
* suite must assert more than the tag. An opaque refusal is correct on the wire
|
|
73
|
+
* and is exactly what blinds this assertion — two different denials share one
|
|
74
|
+
* tag, so a spec can pass on a refusal its author did not mean. An
|
|
75
|
+
* `AppendConditionFailed` now also fires for an EARLIER hop, so the tag no
|
|
76
|
+
* longer says which boundary moved. And `NoReadsRecorded` is a reachable spec
|
|
77
|
+
* whose shape does not mark it as structural: it reads like a domain assertion
|
|
78
|
+
* and is a statement about wiring.
|
|
53
79
|
*/
|
|
54
80
|
readonly thenError: (tag: string) => Effect.Effect<void, never, DcbEventStore | Serializer | Uuid | R | PR>;
|
|
55
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Begin a specification for ANY command cycle that returns the appended
|
|
84
|
+
* `Position` — a bracket for one command as readily as a decider.
|
|
85
|
+
*
|
|
86
|
+
* This is the general form and `specificationFor` below is a one-line
|
|
87
|
+
* specialisation of it, which is the result worth stating rather than the
|
|
88
|
+
* mechanism: a `Decider` is one particular `(command) => Effect<Position, …>`,
|
|
89
|
+
* and `runDecider` is the function that says so. Nothing downstream of `when`
|
|
90
|
+
* moves — the same seed boundary, the same read-back, the same shape comparison
|
|
91
|
+
* — because both cycles already agree on what they return.
|
|
92
|
+
*
|
|
93
|
+
* ⚠ `run`'s error channel is written `E | CycleError` and must be: the chain adds
|
|
94
|
+
* that union itself, so a naive `Effect<Position, E, R>` double-counts it and
|
|
95
|
+
* fails to unify. `CycleError` exists to be written here.
|
|
96
|
+
*/
|
|
97
|
+
export declare const specificationForCycle: <Command, E, R, PR = never>(run: (command: Command) => Effect.Effect<Position, E | CycleError, DcbEventStore | Serializer | Uuid | R | PR>) => Specification<Command, E, R, PR>;
|
|
56
98
|
/**
|
|
57
99
|
* Begin a specification for `decider`. The returned `given(...)` starts the
|
|
58
100
|
* Given/When/Then chain; the terminal step yields an `Effect` requiring
|
|
59
101
|
* `DcbEventStore | Serializer | Uuid | R | RequirementOf<P>`.
|
|
102
|
+
*
|
|
103
|
+
* One line over the layered sibling above, and deliberately not a twin of it:
|
|
104
|
+
* a duplicated body would be two sets of tests and two things free to drift on
|
|
105
|
+
* the seed boundary, the read-back or the shape comparison.
|
|
60
106
|
*/
|
|
61
107
|
export declare const specificationFor: <P extends Record<string, SliceProjection>, Command, E, R>(decider: Decider<P, Command, E, R>) => Specification<Command, E, R, RequirementOf<P>>;
|
|
62
108
|
/**
|
|
@@ -72,5 +118,5 @@ export declare const specificationFor: <P extends Record<string, SliceProjection
|
|
|
72
118
|
* encryption requirement provides its cipher layer inside `build` before
|
|
73
119
|
* returning.
|
|
74
120
|
*/
|
|
75
|
-
export declare const scenario: <P extends Record<string, SliceProjection>, Command, E>(description: string, decider: Decider<P, Command, E>, build: (spec: Specification<Command, E, never, RequirementOf<P>>) => Effect.Effect<void, E |
|
|
121
|
+
export declare const scenario: <P extends Record<string, SliceProjection>, Command, E>(description: string, decider: Decider<P, Command, E>, build: (spec: Specification<Command, E, never, RequirementOf<P>>) => Effect.Effect<void, E | CycleError, DcbEventStore | Serializer | Uuid>) => void;
|
|
76
122
|
//# sourceMappingURL=specification.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specification.d.ts","sourceRoot":"","sources":["../../src/specification.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"specification.d.ts","sourceRoot":"","sources":["../../src/specification.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @kairos-es/testing — the Given/When/Then decision-model testing surface.
|
|
3
3
|
*
|
|
4
|
-
* A `DeciderSpecification`-equivalent over
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A `DeciderSpecification`-equivalent over a real fold and a `DcbEventStore` (in
|
|
5
|
+
* practice the in-memory layer): `@kairos-es/codec`'s `buildDecisionModel` for
|
|
6
|
+
* a single-hop decision, and `modelAtHead` plus the read ledger for a layered
|
|
7
|
+
* one. Shaped so an Event Modeling `Specification` (given events / when a
|
|
7
8
|
* command / then events / thenError a tag) compiles onto it 1:1.
|
|
8
9
|
*
|
|
9
10
|
* scenario('subscribe a student', subscribeDecider, (spec) =>
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,cAAc,oBAAiB","ignoreList":[]}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Given/When/Then surface: a `DeciderSpecification`-equivalent over
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The Given/When/Then surface: a `DeciderSpecification`-equivalent over a real
|
|
3
|
+
* fold and a `DcbEventStore` (the in-memory layer in practice) —
|
|
4
|
+
* `@kairos-es/codec`'s `buildDecisionModel` for a single-hop decision, and
|
|
5
|
+
* `modelAtHead` plus the read ledger for a layered one. `specificationForCycle`
|
|
6
|
+
* is the general entry point over either, named for the command CYCLE it takes
|
|
7
|
+
* rather than for the layered bracket that was its first caller — the shape it
|
|
8
|
+
* accepts is `(command) => Effect<Position, …>` and a decider is one instance of
|
|
9
|
+
* it. It is shaped so an Event Modeling
|
|
5
10
|
* `Specification` (given events, when a command, then events / thenError a tag)
|
|
6
11
|
* compiles onto it 1:1 — prior art: dcb-events `TestRunner` and Emmett's
|
|
7
12
|
* `DeciderSpecification`.
|
|
@@ -84,21 +89,31 @@ const appendedAfter = after => Effect.gen(function* () {
|
|
|
84
89
|
return yield* collect(events);
|
|
85
90
|
});
|
|
86
91
|
/**
|
|
87
|
-
* Begin a specification for
|
|
88
|
-
*
|
|
89
|
-
*
|
|
92
|
+
* Begin a specification for ANY command cycle that returns the appended
|
|
93
|
+
* `Position` — a bracket for one command as readily as a decider.
|
|
94
|
+
*
|
|
95
|
+
* This is the general form and `specificationFor` below is a one-line
|
|
96
|
+
* specialisation of it, which is the result worth stating rather than the
|
|
97
|
+
* mechanism: a `Decider` is one particular `(command) => Effect<Position, …>`,
|
|
98
|
+
* and `runDecider` is the function that says so. Nothing downstream of `when`
|
|
99
|
+
* moves — the same seed boundary, the same read-back, the same shape comparison
|
|
100
|
+
* — because both cycles already agree on what they return.
|
|
101
|
+
*
|
|
102
|
+
* ⚠ `run`'s error channel is written `E | CycleError` and must be: the chain adds
|
|
103
|
+
* that union itself, so a naive `Effect<Position, E, R>` double-counts it and
|
|
104
|
+
* fails to unify. `CycleError` exists to be written here.
|
|
90
105
|
*/
|
|
91
|
-
export const
|
|
106
|
+
export const specificationForCycle = run => ({
|
|
92
107
|
given: (...pastEvents) => ({
|
|
93
108
|
when: command => {
|
|
94
|
-
// Run the seed +
|
|
109
|
+
// Run the seed + cycle, exposing the outcome as an Exit so both
|
|
95
110
|
// branches (then / thenError) can inspect success or failure uniformly. The
|
|
96
111
|
// seed is inside the Exit too, so a decode failure while materialising a
|
|
97
112
|
// given producer is captured as a failure rather than escaping the channel.
|
|
98
113
|
// The success value pairs the seed boundary (the last seeded position) with
|
|
99
|
-
// the
|
|
114
|
+
// the cycle's outcome, so `then` reads strictly after that boundary
|
|
100
115
|
// rather than dropping a fixed row count (correct on a gappy store).
|
|
101
|
-
const runToExit = seed(pastEvents).pipe(Effect.zip(
|
|
116
|
+
const runToExit = seed(pastEvents).pipe(Effect.zip(run(command)), Effect.exit);
|
|
102
117
|
return {
|
|
103
118
|
// biome-ignore lint/suspicious/noThenProperty: deliberate Given/When/Then terminal name (see WhenSpec.then); never awaited as a thenable.
|
|
104
119
|
then: (...expected) => Effect.gen(function* () {
|
|
@@ -128,6 +143,16 @@ export const specificationFor = decider => ({
|
|
|
128
143
|
}
|
|
129
144
|
})
|
|
130
145
|
});
|
|
146
|
+
/**
|
|
147
|
+
* Begin a specification for `decider`. The returned `given(...)` starts the
|
|
148
|
+
* Given/When/Then chain; the terminal step yields an `Effect` requiring
|
|
149
|
+
* `DcbEventStore | Serializer | Uuid | R | RequirementOf<P>`.
|
|
150
|
+
*
|
|
151
|
+
* One line over the layered sibling above, and deliberately not a twin of it:
|
|
152
|
+
* a duplicated body would be two sets of tests and two things free to drift on
|
|
153
|
+
* the seed boundary, the read-back or the shape comparison.
|
|
154
|
+
*/
|
|
155
|
+
export const specificationFor = decider => specificationForCycle(command => runDecider(decider, command));
|
|
131
156
|
/**
|
|
132
157
|
* `scenario(description, spec => spec.given(...).when(...).then(...))` — register
|
|
133
158
|
* an `@effect/vitest` `it.effect` case running `build` against a fresh in-memory
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specification.js","names":["expect","it","runDecider","SerializerDefault","UuidTest","DcbEventStore","DcbEventStoreInMemory","ORIGIN","query","Q","Cause","Chunk","Effect","Exit","Layer","Stream","shapeOf","event","type","tags","data","collect","events","runCollect","pipe","map","toReadonlyArray","seed","producers","gen","store","last","producer","append","orDie","appendedAfter","after","read","all","
|
|
1
|
+
{"version":3,"file":"specification.js","names":["expect","it","runDecider","SerializerDefault","UuidTest","DcbEventStore","DcbEventStoreInMemory","ORIGIN","query","Q","Cause","Chunk","Effect","Exit","Layer","Stream","shapeOf","event","type","tags","data","collect","events","runCollect","pipe","map","toReadonlyArray","seed","producers","gen","store","last","producer","append","orDie","appendedAfter","after","read","all","specificationForCycle","run","given","pastEvents","when","command","runToExit","zip","exit","then","expected","isFailure","failCause","cause","seedBoundary","value","appended","expectedEvents","s","toEqual","thenError","tag","toBe","failure","failureOption","error","_tag","undefined","actualTag","specificationFor","decider","scenario","description","build","effect","provide","mergeAll","fresh"],"sources":["../../src/specification.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kairos-es/testing",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
4
|
"description": "Given/When/Then decision-model testing surface for kairos-es",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"repository": {
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"peerDependencies": {
|
|
14
14
|
"@effect/vitest": "^0.30.0",
|
|
15
15
|
"effect": "^3.22.0",
|
|
16
|
-
"@kairos-es/codec": "^0.0.
|
|
17
|
-
"@kairos-es/core": "^0.0.
|
|
16
|
+
"@kairos-es/codec": "^1.0.0-alpha.1",
|
|
17
|
+
"@kairos-es/core": "^1.0.0-alpha.1"
|
|
18
18
|
},
|
|
19
19
|
"main": "./dist/cjs/index.js",
|
|
20
20
|
"module": "./dist/esm/index.js",
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @kairos-es/testing — the Given/When/Then decision-model testing surface.
|
|
3
3
|
*
|
|
4
|
-
* A `DeciderSpecification`-equivalent over
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A `DeciderSpecification`-equivalent over a real fold and a `DcbEventStore` (in
|
|
5
|
+
* practice the in-memory layer): `@kairos-es/codec`'s `buildDecisionModel` for
|
|
6
|
+
* a single-hop decision, and `modelAtHead` plus the read ledger for a layered
|
|
7
|
+
* one. Shaped so an Event Modeling `Specification` (given events / when a
|
|
7
8
|
* command / then events / thenError a tag) compiles onto it 1:1.
|
|
8
9
|
*
|
|
9
10
|
* scenario('subscribe a student', subscribeDecider, (spec) =>
|
package/src/specification.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Given/When/Then surface: a `DeciderSpecification`-equivalent over
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The Given/When/Then surface: a `DeciderSpecification`-equivalent over a real
|
|
3
|
+
* fold and a `DcbEventStore` (the in-memory layer in practice) —
|
|
4
|
+
* `@kairos-es/codec`'s `buildDecisionModel` for a single-hop decision, and
|
|
5
|
+
* `modelAtHead` plus the read ledger for a layered one. `specificationForCycle`
|
|
6
|
+
* is the general entry point over either, named for the command CYCLE it takes
|
|
7
|
+
* rather than for the layered bracket that was its first caller — the shape it
|
|
8
|
+
* accepts is `(command) => Effect<Position, …>` and a decider is one instance of
|
|
9
|
+
* it. It is shaped so an Event Modeling
|
|
5
10
|
* `Specification` (given events, when a command, then events / thenError a tag)
|
|
6
11
|
* compiles onto it 1:1 — prior art: dcb-events `TestRunner` and Emmett's
|
|
7
12
|
* `DeciderSpecification`.
|
|
@@ -38,8 +43,10 @@ import { expect, it } from '@effect/vitest'
|
|
|
38
43
|
import type {
|
|
39
44
|
CodecError,
|
|
40
45
|
Decider,
|
|
46
|
+
NoReadsRecorded,
|
|
41
47
|
RequirementOf,
|
|
42
48
|
SliceProjection,
|
|
49
|
+
TaglessSliceInLedger,
|
|
43
50
|
} from '@kairos-es/codec'
|
|
44
51
|
import {
|
|
45
52
|
runDecider,
|
|
@@ -128,6 +135,28 @@ const appendedAfter = (
|
|
|
128
135
|
return yield* collect(events)
|
|
129
136
|
})
|
|
130
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The errors a command CYCLE contributes, beyond whatever `E` the decision
|
|
140
|
+
* itself fails with.
|
|
141
|
+
*
|
|
142
|
+
* Named once and exported because it is what makes the layered entry point below
|
|
143
|
+
* UNIFY. `WhenSpec.then` adds this union itself, so a `run` written as a naive
|
|
144
|
+
* `Effect<Position, E, R>` double-counts every member of it and fails to
|
|
145
|
+
* typecheck against a chain that has already added them — a trap with an
|
|
146
|
+
* unhelpful error message, met once and closed here by giving the union a name a
|
|
147
|
+
* caller can write.
|
|
148
|
+
*
|
|
149
|
+
* `NoReadsRecorded` is reachable only on the layered path and `TaglessSliceInLedger`
|
|
150
|
+
* only where a bracket is open, but both are members here rather than in a second
|
|
151
|
+
* union, because the two paths share every step downstream of `when` and a second
|
|
152
|
+
* union would be a second thing to keep in step for no caller's benefit.
|
|
153
|
+
*/
|
|
154
|
+
export type CycleError =
|
|
155
|
+
| CodecError
|
|
156
|
+
| AppendConditionFailed
|
|
157
|
+
| TaglessSliceInLedger
|
|
158
|
+
| NoReadsRecorded
|
|
159
|
+
|
|
131
160
|
/**
|
|
132
161
|
* The entry point of the Given/When/Then chain: what `specificationFor` returns
|
|
133
162
|
* and what `scenario` hands its `build` callback. It is NAMED rather than
|
|
@@ -169,21 +198,30 @@ export interface WhenSpec<E, R, PR> {
|
|
|
169
198
|
* Assert the success branch: the decider appended exactly these events
|
|
170
199
|
* (compared structurally by type/tags/data, in order). The expected events are
|
|
171
200
|
* effectful producers, materialised under the provided layers. The error
|
|
172
|
-
* channel keeps `E |
|
|
173
|
-
*
|
|
174
|
-
*
|
|
201
|
+
* channel keeps `E | CycleError` because a genuinely failing cycle re-raises
|
|
202
|
+
* its cause (a `.then` on a failing decision is a test-authoring mistake, made
|
|
203
|
+
* loud rather than silently green).
|
|
175
204
|
*/
|
|
176
205
|
readonly then: (
|
|
177
206
|
...expected: ReadonlyArray<EventProducer<R>>
|
|
178
207
|
) => Effect.Effect<
|
|
179
208
|
void,
|
|
180
|
-
E |
|
|
209
|
+
E | CycleError,
|
|
181
210
|
DcbEventStore | Serializer | Uuid | R | PR
|
|
182
211
|
>
|
|
183
212
|
/**
|
|
184
213
|
* Assert the failure branch: the cycle failed with a tagged error whose
|
|
185
214
|
* `_tag` equals `tag` (a domain error from `decide`, a `CodecError` from
|
|
186
215
|
* decode, or `AppendConditionFailed` from a moved consistency boundary).
|
|
216
|
+
*
|
|
217
|
+
* ⚠ THREE WAYS THIS PASSES FOR THE WRONG REASON, and a layered or permission
|
|
218
|
+
* suite must assert more than the tag. An opaque refusal is correct on the wire
|
|
219
|
+
* and is exactly what blinds this assertion — two different denials share one
|
|
220
|
+
* tag, so a spec can pass on a refusal its author did not mean. An
|
|
221
|
+
* `AppendConditionFailed` now also fires for an EARLIER hop, so the tag no
|
|
222
|
+
* longer says which boundary moved. And `NoReadsRecorded` is a reachable spec
|
|
223
|
+
* whose shape does not mark it as structural: it reads like a domain assertion
|
|
224
|
+
* and is a statement about wiring.
|
|
187
225
|
*/
|
|
188
226
|
readonly thenError: (
|
|
189
227
|
tag: string,
|
|
@@ -191,29 +229,40 @@ export interface WhenSpec<E, R, PR> {
|
|
|
191
229
|
}
|
|
192
230
|
|
|
193
231
|
/**
|
|
194
|
-
* Begin a specification for
|
|
195
|
-
*
|
|
196
|
-
*
|
|
232
|
+
* Begin a specification for ANY command cycle that returns the appended
|
|
233
|
+
* `Position` — a bracket for one command as readily as a decider.
|
|
234
|
+
*
|
|
235
|
+
* This is the general form and `specificationFor` below is a one-line
|
|
236
|
+
* specialisation of it, which is the result worth stating rather than the
|
|
237
|
+
* mechanism: a `Decider` is one particular `(command) => Effect<Position, …>`,
|
|
238
|
+
* and `runDecider` is the function that says so. Nothing downstream of `when`
|
|
239
|
+
* moves — the same seed boundary, the same read-back, the same shape comparison
|
|
240
|
+
* — because both cycles already agree on what they return.
|
|
241
|
+
*
|
|
242
|
+
* ⚠ `run`'s error channel is written `E | CycleError` and must be: the chain adds
|
|
243
|
+
* that union itself, so a naive `Effect<Position, E, R>` double-counts it and
|
|
244
|
+
* fails to unify. `CycleError` exists to be written here.
|
|
197
245
|
*/
|
|
198
|
-
export const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
246
|
+
export const specificationForCycle = <Command, E, R, PR = never>(
|
|
247
|
+
run: (
|
|
248
|
+
command: Command,
|
|
249
|
+
) => Effect.Effect<
|
|
250
|
+
Position,
|
|
251
|
+
E | CycleError,
|
|
252
|
+
DcbEventStore | Serializer | Uuid | R | PR
|
|
253
|
+
>,
|
|
254
|
+
): Specification<Command, E, R, PR> => ({
|
|
206
255
|
given: (...pastEvents) => ({
|
|
207
256
|
when: (command) => {
|
|
208
|
-
// Run the seed +
|
|
257
|
+
// Run the seed + cycle, exposing the outcome as an Exit so both
|
|
209
258
|
// branches (then / thenError) can inspect success or failure uniformly. The
|
|
210
259
|
// seed is inside the Exit too, so a decode failure while materialising a
|
|
211
260
|
// given producer is captured as a failure rather than escaping the channel.
|
|
212
261
|
// The success value pairs the seed boundary (the last seeded position) with
|
|
213
|
-
// the
|
|
262
|
+
// the cycle's outcome, so `then` reads strictly after that boundary
|
|
214
263
|
// rather than dropping a fixed row count (correct on a gappy store).
|
|
215
264
|
const runToExit = seed(pastEvents).pipe(
|
|
216
|
-
Effect.zip(
|
|
265
|
+
Effect.zip(run(command)),
|
|
217
266
|
Effect.exit,
|
|
218
267
|
)
|
|
219
268
|
|
|
@@ -254,6 +303,27 @@ export const specificationFor = <
|
|
|
254
303
|
}),
|
|
255
304
|
})
|
|
256
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Begin a specification for `decider`. The returned `given(...)` starts the
|
|
308
|
+
* Given/When/Then chain; the terminal step yields an `Effect` requiring
|
|
309
|
+
* `DcbEventStore | Serializer | Uuid | R | RequirementOf<P>`.
|
|
310
|
+
*
|
|
311
|
+
* One line over the layered sibling above, and deliberately not a twin of it:
|
|
312
|
+
* a duplicated body would be two sets of tests and two things free to drift on
|
|
313
|
+
* the seed boundary, the read-back or the shape comparison.
|
|
314
|
+
*/
|
|
315
|
+
export const specificationFor = <
|
|
316
|
+
P extends Record<string, SliceProjection>,
|
|
317
|
+
Command,
|
|
318
|
+
E,
|
|
319
|
+
R,
|
|
320
|
+
>(
|
|
321
|
+
decider: Decider<P, Command, E, R>,
|
|
322
|
+
): Specification<Command, E, R, RequirementOf<P>> =>
|
|
323
|
+
specificationForCycle<Command, E, R, RequirementOf<P>>((command) =>
|
|
324
|
+
runDecider(decider, command),
|
|
325
|
+
)
|
|
326
|
+
|
|
257
327
|
/**
|
|
258
328
|
* `scenario(description, spec => spec.given(...).when(...).then(...))` — register
|
|
259
329
|
* an `@effect/vitest` `it.effect` case running `build` against a fresh in-memory
|
|
@@ -272,11 +342,7 @@ export const scenario = <P extends Record<string, SliceProjection>, Command, E>(
|
|
|
272
342
|
decider: Decider<P, Command, E>,
|
|
273
343
|
build: (
|
|
274
344
|
spec: Specification<Command, E, never, RequirementOf<P>>,
|
|
275
|
-
) => Effect.Effect<
|
|
276
|
-
void,
|
|
277
|
-
E | CodecError | AppendConditionFailed,
|
|
278
|
-
DcbEventStore | Serializer | Uuid
|
|
279
|
-
>,
|
|
345
|
+
) => Effect.Effect<void, E | CycleError, DcbEventStore | Serializer | Uuid>,
|
|
280
346
|
): void => {
|
|
281
347
|
it.effect(description, () =>
|
|
282
348
|
build(specificationFor(decider)).pipe(
|