@kairos-es/testing 1.0.0-alpha.1 → 1.0.0-alpha.3
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 +5 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/specification.js +20 -3
- package/dist/cjs/specification.js.map +1 -1
- package/dist/dts/index.d.ts +3 -1
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/specification.d.ts +26 -7
- package/dist/dts/specification.d.ts.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/specification.js +21 -4
- package/dist/esm/specification.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +3 -1
- package/src/specification.ts +66 -9
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A **development** dependency: it peers `@effect/vitest` and is imported from
|
|
|
8
8
|
tests only.
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
pnpm add -D @kairos-es/testing@alpha @effect/vitest vitest
|
|
11
|
+
pnpm add -D @kairos-es/testing@alpha @effect/vitest@^0.30.0 vitest@^3.2.4
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
`vitest` is named because `@effect/vitest` requires it and pnpm does not install
|
|
@@ -39,6 +39,10 @@ which boundary moved. And `NoReadsRecorded` reads like a domain assertion while
|
|
|
39
39
|
being a statement about wiring. A layered or permission suite should assert more
|
|
40
40
|
than the tag.
|
|
41
41
|
|
|
42
|
+
`.thenUnchanged()` asserts the "already done" branch: the cycle succeeded with
|
|
43
|
+
the codec's `Unchanged` AND appended nothing. `.then()` and `.thenError(tag)` both
|
|
44
|
+
fail on an `Unchanged` cycle, so the three terminals cannot pass for one another.
|
|
45
|
+
|
|
42
46
|
The `Decider` a scenario exercises and the `runDecider` that runs it are
|
|
43
47
|
production symbols and belong to [`@kairos-es/codec`](../codec); this package
|
|
44
48
|
imports them and deliberately does not re-export them, so typing a command
|
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":";;;;;AA6BA,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":[]}
|
|
@@ -31,12 +31,17 @@ var _effect = require("effect");
|
|
|
31
31
|
* .when(command)
|
|
32
32
|
* .thenError('AppendConditionFailed') // failure branch (domain error or conflict)
|
|
33
33
|
*
|
|
34
|
+
* specificationFor(decider)
|
|
35
|
+
* .given(...pastEventProducers)
|
|
36
|
+
* .when(command)
|
|
37
|
+
* .thenUnchanged() // "already done" branch: nothing appended
|
|
38
|
+
*
|
|
34
39
|
* Events are supplied *effectfully*: a given/expected event is the producer an
|
|
35
40
|
* application authors via `toEvent` — an `Effect<DcbEvent, CodecError,
|
|
36
41
|
* Serializer | Uuid | R>` — so both sides materialise (encode -> serialise)
|
|
37
42
|
* under the same layers the cycle runs against, and their `data` bytes match.
|
|
38
43
|
*
|
|
39
|
-
* Each terminal (`then` / `thenError`) returns an `Effect` requiring
|
|
44
|
+
* Each terminal (`then` / `thenError` / `thenUnchanged`) returns an `Effect` requiring
|
|
40
45
|
* `DcbEventStore | Serializer | Uuid | R`, so a test provides the store,
|
|
41
46
|
* serialiser, and uuid layers (fresh store per case for isolation).
|
|
42
47
|
* `scenario(...)` wraps the whole thing in `@effect/vitest`'s `it.effect`
|
|
@@ -97,7 +102,8 @@ const appendedAfter = after => _effect.Effect.gen(function* () {
|
|
|
97
102
|
});
|
|
98
103
|
/**
|
|
99
104
|
* Begin a specification for ANY command cycle that returns the appended
|
|
100
|
-
* `Position` — a bracket for one command as readily as a decider
|
|
105
|
+
* `Position` — a bracket for one command as readily as a decider — or
|
|
106
|
+
* `Unchanged`, for a cycle that can answer "already done".
|
|
101
107
|
*
|
|
102
108
|
* This is the general form and `specificationFor` below is a one-line
|
|
103
109
|
* specialisation of it, which is the result worth stating rather than the
|
|
@@ -131,7 +137,8 @@ const specificationForCycle = run => ({
|
|
|
131
137
|
// it loud rather than silently passing.
|
|
132
138
|
return yield* _effect.Effect.failCause(exit.cause);
|
|
133
139
|
}
|
|
134
|
-
const [seedBoundary] = exit.value;
|
|
140
|
+
const [seedBoundary, result] = exit.value;
|
|
141
|
+
(0, _vitest.expect)((0, _codec.isUnchanged)(result)).toBe(false);
|
|
135
142
|
const appended = yield* appendedAfter(seedBoundary);
|
|
136
143
|
const expectedEvents = yield* _effect.Effect.all(expected);
|
|
137
144
|
(0, _vitest.expect)(appended.map(s => shapeOf(s.event))).toEqual(expectedEvents.map(shapeOf));
|
|
@@ -145,6 +152,16 @@ const specificationForCycle = run => ({
|
|
|
145
152
|
const actualTag = typeof error === 'object' && error !== null && '_tag' in error ? error._tag : undefined;
|
|
146
153
|
(0, _vitest.expect)(actualTag).toBe(tag);
|
|
147
154
|
}
|
|
155
|
+
}),
|
|
156
|
+
thenUnchanged: () => _effect.Effect.gen(function* () {
|
|
157
|
+
const exit = yield* runToExit;
|
|
158
|
+
if (_effect.Exit.isFailure(exit)) {
|
|
159
|
+
return yield* _effect.Effect.failCause(exit.cause);
|
|
160
|
+
}
|
|
161
|
+
const [seedBoundary, result] = exit.value;
|
|
162
|
+
(0, _vitest.expect)((0, _codec.isUnchanged)(result)).toBe(true);
|
|
163
|
+
const appended = yield* appendedAfter(seedBoundary);
|
|
164
|
+
(0, _vitest.expect)(appended.map(s => shapeOf(s.event))).toEqual([]);
|
|
148
165
|
})
|
|
149
166
|
};
|
|
150
167
|
}
|
|
@@ -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","specificationForCycle","run","given","pastEvents","when","command","runToExit","zip","exit","then","expected","Exit","isFailure","failCause","cause","seedBoundary","value","
|
|
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","result","value","expect","isUnchanged","toBe","appended","expectedEvents","s","toEqual","thenError","tag","failure","Cause","failureOption","error","_tag","undefined","actualTag","thenUnchanged","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
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* practice the in-memory layer): `@kairos-es/codec`'s `buildDecisionModel` for
|
|
6
6
|
* a single-hop decision, and `modelAtHead` plus the read ledger for a layered
|
|
7
7
|
* one. Shaped so an Event Modeling `Specification` (given events / when a
|
|
8
|
-
* command / then events / thenError a tag) compiles onto it 1:1
|
|
8
|
+
* command / then events / thenError a tag) compiles onto it 1:1, plus
|
|
9
|
+
* `thenUnchanged` for a decision that answers "already done" and appends
|
|
10
|
+
* nothing.
|
|
9
11
|
*
|
|
10
12
|
* scenario('subscribe a student', subscribeDecider, (spec) =>
|
|
11
13
|
* 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,cAAc,iBAAiB,CAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { CodecError, Decider, NoReadsRecorded, RequirementOf, SliceProjection, TaglessSliceInLedger } from '@kairos-es/codec';
|
|
1
|
+
import type { CodecError, Decider, DecisionOutput, NoReadsRecorded, RequirementOf, SliceProjection, TaglessSliceInLedger, Unchanged } from '@kairos-es/codec';
|
|
2
2
|
import { type Serializer, type Uuid } from '@kairos-es/codec';
|
|
3
3
|
import { type AppendConditionFailed, type DcbEvent, DcbEventStore, type Position } from '@kairos-es/core';
|
|
4
|
-
import { Effect } from 'effect';
|
|
4
|
+
import { type Array as Arr, 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
7
|
/**
|
|
@@ -52,7 +52,7 @@ export interface GivenSpec<Command, E, R, PR> {
|
|
|
52
52
|
/** Supply the command to decide. */
|
|
53
53
|
readonly when: (command: Command) => WhenSpec<E, R, PR>;
|
|
54
54
|
}
|
|
55
|
-
/** A spec that has a command and awaits a `then`/`thenError` assertion. */
|
|
55
|
+
/** A spec that has a command and awaits a `then`/`thenError`/`thenUnchanged` assertion. */
|
|
56
56
|
export interface WhenSpec<E, R, PR> {
|
|
57
57
|
/**
|
|
58
58
|
* Assert the success branch: the decider appended exactly these events
|
|
@@ -61,6 +61,12 @@ export interface WhenSpec<E, R, PR> {
|
|
|
61
61
|
* channel keeps `E | CycleError` because a genuinely failing cycle re-raises
|
|
62
62
|
* its cause (a `.then` on a failing decision is a test-authoring mistake, made
|
|
63
63
|
* loud rather than silently green).
|
|
64
|
+
*
|
|
65
|
+
* A cycle that answered `Unchanged` FAILS this assertion, even with no
|
|
66
|
+
* expected events. `.then()` with an empty list and `.thenUnchanged()` both
|
|
67
|
+
* see nothing appended, but they claim different things — *decided an empty
|
|
68
|
+
* change* is not a decision this library can make, while *already done* is —
|
|
69
|
+
* so the one that does not match the cycle's answer must not pass.
|
|
64
70
|
*/
|
|
65
71
|
readonly then: (...expected: ReadonlyArray<EventProducer<R>>) => Effect.Effect<void, E | CycleError, DcbEventStore | Serializer | Uuid | R | PR>;
|
|
66
72
|
/**
|
|
@@ -78,10 +84,23 @@ export interface WhenSpec<E, R, PR> {
|
|
|
78
84
|
* and is a statement about wiring.
|
|
79
85
|
*/
|
|
80
86
|
readonly thenError: (tag: string) => Effect.Effect<void, never, DcbEventStore | Serializer | Uuid | R | PR>;
|
|
87
|
+
/**
|
|
88
|
+
* Assert the "already done" branch: the cycle SUCCEEDED with `Unchanged`
|
|
89
|
+
* (recognised by `isUnchanged`, never by its tag) and appended nothing past
|
|
90
|
+
* the seed. A cycle that appended — even an empty-looking one — or that
|
|
91
|
+
* returned a `Position` fails it, and a failing cycle re-raises its cause, for
|
|
92
|
+
* the reason `then` gives.
|
|
93
|
+
*
|
|
94
|
+
* Both halves are asserted, not just the result, because they are different
|
|
95
|
+
* claims: a runner that returned `Unchanged` AND appended would be exactly the
|
|
96
|
+
* defect this step exists to catch, and the result alone would not see it.
|
|
97
|
+
*/
|
|
98
|
+
readonly thenUnchanged: () => Effect.Effect<void, E | CycleError, DcbEventStore | Serializer | Uuid | R | PR>;
|
|
81
99
|
}
|
|
82
100
|
/**
|
|
83
101
|
* Begin a specification for ANY command cycle that returns the appended
|
|
84
|
-
* `Position` — a bracket for one command as readily as a decider
|
|
102
|
+
* `Position` — a bracket for one command as readily as a decider — or
|
|
103
|
+
* `Unchanged`, for a cycle that can answer "already done".
|
|
85
104
|
*
|
|
86
105
|
* This is the general form and `specificationFor` below is a one-line
|
|
87
106
|
* specialisation of it, which is the result worth stating rather than the
|
|
@@ -94,7 +113,7 @@ export interface WhenSpec<E, R, PR> {
|
|
|
94
113
|
* that union itself, so a naive `Effect<Position, E, R>` double-counts it and
|
|
95
114
|
* fails to unify. `CycleError` exists to be written here.
|
|
96
115
|
*/
|
|
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>;
|
|
116
|
+
export declare const specificationForCycle: <Command, E, R, PR = never>(run: (command: Command) => Effect.Effect<Position | Unchanged, E | CycleError, DcbEventStore | Serializer | Uuid | R | PR>) => Specification<Command, E, R, PR>;
|
|
98
117
|
/**
|
|
99
118
|
* Begin a specification for `decider`. The returned `given(...)` starts the
|
|
100
119
|
* Given/When/Then chain; the terminal step yields an `Effect` requiring
|
|
@@ -104,7 +123,7 @@ export declare const specificationForCycle: <Command, E, R, PR = never>(run: (co
|
|
|
104
123
|
* a duplicated body would be two sets of tests and two things free to drift on
|
|
105
124
|
* the seed boundary, the read-back or the shape comparison.
|
|
106
125
|
*/
|
|
107
|
-
export declare const specificationFor: <P extends Record<string, SliceProjection>, Command, E, R
|
|
126
|
+
export declare const specificationFor: <P extends Record<string, SliceProjection>, Command, E, R, O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>>(decider: Decider<P, Command, E, R, O>) => Specification<Command, E, R, RequirementOf<P>>;
|
|
108
127
|
/**
|
|
109
128
|
* `scenario(description, spec => spec.given(...).when(...).then(...))` — register
|
|
110
129
|
* an `@effect/vitest` `it.effect` case running `build` against a fresh in-memory
|
|
@@ -118,5 +137,5 @@ export declare const specificationFor: <P extends Record<string, SliceProjection
|
|
|
118
137
|
* encryption requirement provides its cipher layer inside `build` before
|
|
119
138
|
* returning.
|
|
120
139
|
*/
|
|
121
|
-
export declare const scenario: <P extends Record<string, SliceProjection>, Command, E
|
|
140
|
+
export declare const scenario: <P extends Record<string, SliceProjection>, Command, E, O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>>(description: string, decider: Decider<P, Command, E, never, O>, build: (spec: Specification<Command, E, never, RequirementOf<P>>) => Effect.Effect<void, E | CycleError, DcbEventStore | Serializer | Uuid>) => void;
|
|
122
141
|
//# 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
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* practice the in-memory layer): `@kairos-es/codec`'s `buildDecisionModel` for
|
|
6
6
|
* a single-hop decision, and `modelAtHead` plus the read ledger for a layered
|
|
7
7
|
* one. Shaped so an Event Modeling `Specification` (given events / when a
|
|
8
|
-
* command / then events / thenError a tag) compiles onto it 1:1
|
|
8
|
+
* command / then events / thenError a tag) compiles onto it 1:1, plus
|
|
9
|
+
* `thenUnchanged` for a decision that answers "already done" and appends
|
|
10
|
+
* nothing.
|
|
9
11
|
*
|
|
10
12
|
* scenario('subscribe a student', subscribeDecider, (spec) =>
|
|
11
13
|
* 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,cAAc,oBAAiB","ignoreList":[]}
|
|
@@ -21,12 +21,17 @@
|
|
|
21
21
|
* .when(command)
|
|
22
22
|
* .thenError('AppendConditionFailed') // failure branch (domain error or conflict)
|
|
23
23
|
*
|
|
24
|
+
* specificationFor(decider)
|
|
25
|
+
* .given(...pastEventProducers)
|
|
26
|
+
* .when(command)
|
|
27
|
+
* .thenUnchanged() // "already done" branch: nothing appended
|
|
28
|
+
*
|
|
24
29
|
* Events are supplied *effectfully*: a given/expected event is the producer an
|
|
25
30
|
* application authors via `toEvent` — an `Effect<DcbEvent, CodecError,
|
|
26
31
|
* Serializer | Uuid | R>` — so both sides materialise (encode -> serialise)
|
|
27
32
|
* under the same layers the cycle runs against, and their `data` bytes match.
|
|
28
33
|
*
|
|
29
|
-
* Each terminal (`then` / `thenError`) returns an `Effect` requiring
|
|
34
|
+
* Each terminal (`then` / `thenError` / `thenUnchanged`) returns an `Effect` requiring
|
|
30
35
|
* `DcbEventStore | Serializer | Uuid | R`, so a test provides the store,
|
|
31
36
|
* serialiser, and uuid layers (fresh store per case for isolation).
|
|
32
37
|
* `scenario(...)` wraps the whole thing in `@effect/vitest`'s `it.effect`
|
|
@@ -40,7 +45,7 @@
|
|
|
40
45
|
* deterministic, so equal payloads produce equal `data` bytes.
|
|
41
46
|
*/
|
|
42
47
|
import { expect, it } from '@effect/vitest';
|
|
43
|
-
import { runDecider, SerializerDefault, UuidTest } from '@kairos-es/codec';
|
|
48
|
+
import { isUnchanged, runDecider, SerializerDefault, UuidTest } from '@kairos-es/codec';
|
|
44
49
|
import { DcbEventStore, DcbEventStoreInMemory, ORIGIN, query as Q } from '@kairos-es/core';
|
|
45
50
|
import { Cause, Chunk, Effect, Exit, Layer, Stream } from 'effect';
|
|
46
51
|
const shapeOf = event => ({
|
|
@@ -90,7 +95,8 @@ const appendedAfter = after => Effect.gen(function* () {
|
|
|
90
95
|
});
|
|
91
96
|
/**
|
|
92
97
|
* Begin a specification for ANY command cycle that returns the appended
|
|
93
|
-
* `Position` — a bracket for one command as readily as a decider
|
|
98
|
+
* `Position` — a bracket for one command as readily as a decider — or
|
|
99
|
+
* `Unchanged`, for a cycle that can answer "already done".
|
|
94
100
|
*
|
|
95
101
|
* This is the general form and `specificationFor` below is a one-line
|
|
96
102
|
* specialisation of it, which is the result worth stating rather than the
|
|
@@ -124,7 +130,8 @@ export const specificationForCycle = run => ({
|
|
|
124
130
|
// it loud rather than silently passing.
|
|
125
131
|
return yield* Effect.failCause(exit.cause);
|
|
126
132
|
}
|
|
127
|
-
const [seedBoundary] = exit.value;
|
|
133
|
+
const [seedBoundary, result] = exit.value;
|
|
134
|
+
expect(isUnchanged(result)).toBe(false);
|
|
128
135
|
const appended = yield* appendedAfter(seedBoundary);
|
|
129
136
|
const expectedEvents = yield* Effect.all(expected);
|
|
130
137
|
expect(appended.map(s => shapeOf(s.event))).toEqual(expectedEvents.map(shapeOf));
|
|
@@ -138,6 +145,16 @@ export const specificationForCycle = run => ({
|
|
|
138
145
|
const actualTag = typeof error === 'object' && error !== null && '_tag' in error ? error._tag : undefined;
|
|
139
146
|
expect(actualTag).toBe(tag);
|
|
140
147
|
}
|
|
148
|
+
}),
|
|
149
|
+
thenUnchanged: () => Effect.gen(function* () {
|
|
150
|
+
const exit = yield* runToExit;
|
|
151
|
+
if (Exit.isFailure(exit)) {
|
|
152
|
+
return yield* Effect.failCause(exit.cause);
|
|
153
|
+
}
|
|
154
|
+
const [seedBoundary, result] = exit.value;
|
|
155
|
+
expect(isUnchanged(result)).toBe(true);
|
|
156
|
+
const appended = yield* appendedAfter(seedBoundary);
|
|
157
|
+
expect(appended.map(s => shapeOf(s.event))).toEqual([]);
|
|
141
158
|
})
|
|
142
159
|
};
|
|
143
160
|
}
|
|
@@ -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","specificationForCycle","run","given","pastEvents","when","command","runToExit","zip","exit","then","expected","isFailure","failCause","cause","seedBoundary","value","appended","expectedEvents","s","toEqual","thenError","tag","
|
|
1
|
+
{"version":3,"file":"specification.js","names":["expect","it","isUnchanged","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","result","value","toBe","appended","expectedEvents","s","toEqual","thenError","tag","failure","failureOption","error","_tag","undefined","actualTag","thenUnchanged","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": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.3",
|
|
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": "^1.0.0-alpha.
|
|
17
|
-
"@kairos-es/core": "^1.0.0-alpha.
|
|
16
|
+
"@kairos-es/codec": "^1.0.0-alpha.3",
|
|
17
|
+
"@kairos-es/core": "^1.0.0-alpha.3"
|
|
18
18
|
},
|
|
19
19
|
"main": "./dist/cjs/index.js",
|
|
20
20
|
"module": "./dist/esm/index.js",
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* practice the in-memory layer): `@kairos-es/codec`'s `buildDecisionModel` for
|
|
6
6
|
* a single-hop decision, and `modelAtHead` plus the read ledger for a layered
|
|
7
7
|
* one. Shaped so an Event Modeling `Specification` (given events / when a
|
|
8
|
-
* command / then events / thenError a tag) compiles onto it 1:1
|
|
8
|
+
* command / then events / thenError a tag) compiles onto it 1:1, plus
|
|
9
|
+
* `thenUnchanged` for a decision that answers "already done" and appends
|
|
10
|
+
* nothing.
|
|
9
11
|
*
|
|
10
12
|
* scenario('subscribe a student', subscribeDecider, (spec) =>
|
|
11
13
|
* spec
|
package/src/specification.ts
CHANGED
|
@@ -21,12 +21,17 @@
|
|
|
21
21
|
* .when(command)
|
|
22
22
|
* .thenError('AppendConditionFailed') // failure branch (domain error or conflict)
|
|
23
23
|
*
|
|
24
|
+
* specificationFor(decider)
|
|
25
|
+
* .given(...pastEventProducers)
|
|
26
|
+
* .when(command)
|
|
27
|
+
* .thenUnchanged() // "already done" branch: nothing appended
|
|
28
|
+
*
|
|
24
29
|
* Events are supplied *effectfully*: a given/expected event is the producer an
|
|
25
30
|
* application authors via `toEvent` — an `Effect<DcbEvent, CodecError,
|
|
26
31
|
* Serializer | Uuid | R>` — so both sides materialise (encode -> serialise)
|
|
27
32
|
* under the same layers the cycle runs against, and their `data` bytes match.
|
|
28
33
|
*
|
|
29
|
-
* Each terminal (`then` / `thenError`) returns an `Effect` requiring
|
|
34
|
+
* Each terminal (`then` / `thenError` / `thenUnchanged`) returns an `Effect` requiring
|
|
30
35
|
* `DcbEventStore | Serializer | Uuid | R`, so a test provides the store,
|
|
31
36
|
* serialiser, and uuid layers (fresh store per case for isolation).
|
|
32
37
|
* `scenario(...)` wraps the whole thing in `@effect/vitest`'s `it.effect`
|
|
@@ -43,12 +48,15 @@ import { expect, it } from '@effect/vitest'
|
|
|
43
48
|
import type {
|
|
44
49
|
CodecError,
|
|
45
50
|
Decider,
|
|
51
|
+
DecisionOutput,
|
|
46
52
|
NoReadsRecorded,
|
|
47
53
|
RequirementOf,
|
|
48
54
|
SliceProjection,
|
|
49
55
|
TaglessSliceInLedger,
|
|
56
|
+
Unchanged,
|
|
50
57
|
} from '@kairos-es/codec'
|
|
51
58
|
import {
|
|
59
|
+
isUnchanged,
|
|
52
60
|
runDecider,
|
|
53
61
|
type Serializer,
|
|
54
62
|
SerializerDefault,
|
|
@@ -65,7 +73,15 @@ import {
|
|
|
65
73
|
query as Q,
|
|
66
74
|
type SequencedEvent,
|
|
67
75
|
} from '@kairos-es/core'
|
|
68
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
type Array as Arr,
|
|
78
|
+
Cause,
|
|
79
|
+
Chunk,
|
|
80
|
+
Effect,
|
|
81
|
+
Exit,
|
|
82
|
+
Layer,
|
|
83
|
+
Stream,
|
|
84
|
+
} from 'effect'
|
|
69
85
|
|
|
70
86
|
/** An effectful producer of a wire event (the `toEvent` result an app authors). */
|
|
71
87
|
export type EventProducer<R> = Effect.Effect<
|
|
@@ -192,7 +208,7 @@ export interface GivenSpec<Command, E, R, PR> {
|
|
|
192
208
|
readonly when: (command: Command) => WhenSpec<E, R, PR>
|
|
193
209
|
}
|
|
194
210
|
|
|
195
|
-
/** A spec that has a command and awaits a `then`/`thenError` assertion. */
|
|
211
|
+
/** A spec that has a command and awaits a `then`/`thenError`/`thenUnchanged` assertion. */
|
|
196
212
|
export interface WhenSpec<E, R, PR> {
|
|
197
213
|
/**
|
|
198
214
|
* Assert the success branch: the decider appended exactly these events
|
|
@@ -201,6 +217,12 @@ export interface WhenSpec<E, R, PR> {
|
|
|
201
217
|
* channel keeps `E | CycleError` because a genuinely failing cycle re-raises
|
|
202
218
|
* its cause (a `.then` on a failing decision is a test-authoring mistake, made
|
|
203
219
|
* loud rather than silently green).
|
|
220
|
+
*
|
|
221
|
+
* A cycle that answered `Unchanged` FAILS this assertion, even with no
|
|
222
|
+
* expected events. `.then()` with an empty list and `.thenUnchanged()` both
|
|
223
|
+
* see nothing appended, but they claim different things — *decided an empty
|
|
224
|
+
* change* is not a decision this library can make, while *already done* is —
|
|
225
|
+
* so the one that does not match the cycle's answer must not pass.
|
|
204
226
|
*/
|
|
205
227
|
readonly then: (
|
|
206
228
|
...expected: ReadonlyArray<EventProducer<R>>
|
|
@@ -226,11 +248,28 @@ export interface WhenSpec<E, R, PR> {
|
|
|
226
248
|
readonly thenError: (
|
|
227
249
|
tag: string,
|
|
228
250
|
) => Effect.Effect<void, never, DcbEventStore | Serializer | Uuid | R | PR>
|
|
251
|
+
/**
|
|
252
|
+
* Assert the "already done" branch: the cycle SUCCEEDED with `Unchanged`
|
|
253
|
+
* (recognised by `isUnchanged`, never by its tag) and appended nothing past
|
|
254
|
+
* the seed. A cycle that appended — even an empty-looking one — or that
|
|
255
|
+
* returned a `Position` fails it, and a failing cycle re-raises its cause, for
|
|
256
|
+
* the reason `then` gives.
|
|
257
|
+
*
|
|
258
|
+
* Both halves are asserted, not just the result, because they are different
|
|
259
|
+
* claims: a runner that returned `Unchanged` AND appended would be exactly the
|
|
260
|
+
* defect this step exists to catch, and the result alone would not see it.
|
|
261
|
+
*/
|
|
262
|
+
readonly thenUnchanged: () => Effect.Effect<
|
|
263
|
+
void,
|
|
264
|
+
E | CycleError,
|
|
265
|
+
DcbEventStore | Serializer | Uuid | R | PR
|
|
266
|
+
>
|
|
229
267
|
}
|
|
230
268
|
|
|
231
269
|
/**
|
|
232
270
|
* Begin a specification for ANY command cycle that returns the appended
|
|
233
|
-
* `Position` — a bracket for one command as readily as a decider
|
|
271
|
+
* `Position` — a bracket for one command as readily as a decider — or
|
|
272
|
+
* `Unchanged`, for a cycle that can answer "already done".
|
|
234
273
|
*
|
|
235
274
|
* This is the general form and `specificationFor` below is a one-line
|
|
236
275
|
* specialisation of it, which is the result worth stating rather than the
|
|
@@ -247,7 +286,7 @@ export const specificationForCycle = <Command, E, R, PR = never>(
|
|
|
247
286
|
run: (
|
|
248
287
|
command: Command,
|
|
249
288
|
) => Effect.Effect<
|
|
250
|
-
Position,
|
|
289
|
+
Position | Unchanged,
|
|
251
290
|
E | CycleError,
|
|
252
291
|
DcbEventStore | Serializer | Uuid | R | PR
|
|
253
292
|
>,
|
|
@@ -277,7 +316,8 @@ export const specificationForCycle = <Command, E, R, PR = never>(
|
|
|
277
316
|
// it loud rather than silently passing.
|
|
278
317
|
return yield* Effect.failCause(exit.cause)
|
|
279
318
|
}
|
|
280
|
-
const [seedBoundary] = exit.value
|
|
319
|
+
const [seedBoundary, result] = exit.value
|
|
320
|
+
expect(isUnchanged(result)).toBe(false)
|
|
281
321
|
const appended = yield* appendedAfter(seedBoundary)
|
|
282
322
|
const expectedEvents = yield* Effect.all(expected)
|
|
283
323
|
expect(appended.map((s) => shapeOf(s.event))).toEqual(
|
|
@@ -298,6 +338,17 @@ export const specificationForCycle = <Command, E, R, PR = never>(
|
|
|
298
338
|
expect(actualTag).toBe(tag)
|
|
299
339
|
}
|
|
300
340
|
}),
|
|
341
|
+
thenUnchanged: () =>
|
|
342
|
+
Effect.gen(function* () {
|
|
343
|
+
const exit = yield* runToExit
|
|
344
|
+
if (Exit.isFailure(exit)) {
|
|
345
|
+
return yield* Effect.failCause(exit.cause)
|
|
346
|
+
}
|
|
347
|
+
const [seedBoundary, result] = exit.value
|
|
348
|
+
expect(isUnchanged(result)).toBe(true)
|
|
349
|
+
const appended = yield* appendedAfter(seedBoundary)
|
|
350
|
+
expect(appended.map((s) => shapeOf(s.event))).toEqual([])
|
|
351
|
+
}),
|
|
301
352
|
}
|
|
302
353
|
},
|
|
303
354
|
}),
|
|
@@ -317,8 +368,9 @@ export const specificationFor = <
|
|
|
317
368
|
Command,
|
|
318
369
|
E,
|
|
319
370
|
R,
|
|
371
|
+
O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>,
|
|
320
372
|
>(
|
|
321
|
-
decider: Decider<P, Command, E, R>,
|
|
373
|
+
decider: Decider<P, Command, E, R, O>,
|
|
322
374
|
): Specification<Command, E, R, RequirementOf<P>> =>
|
|
323
375
|
specificationForCycle<Command, E, R, RequirementOf<P>>((command) =>
|
|
324
376
|
runDecider(decider, command),
|
|
@@ -337,9 +389,14 @@ export const specificationFor = <
|
|
|
337
389
|
* encryption requirement provides its cipher layer inside `build` before
|
|
338
390
|
* returning.
|
|
339
391
|
*/
|
|
340
|
-
export const scenario = <
|
|
392
|
+
export const scenario = <
|
|
393
|
+
P extends Record<string, SliceProjection>,
|
|
394
|
+
Command,
|
|
395
|
+
E,
|
|
396
|
+
O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>,
|
|
397
|
+
>(
|
|
341
398
|
description: string,
|
|
342
|
-
decider: Decider<P, Command, E>,
|
|
399
|
+
decider: Decider<P, Command, E, never, O>,
|
|
343
400
|
build: (
|
|
344
401
|
spec: Specification<Command, E, never, RequirementOf<P>>,
|
|
345
402
|
) => Effect.Effect<void, E | CycleError, DcbEventStore | Serializer | Uuid>,
|