@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 CHANGED
@@ -2,10 +2,42 @@
2
2
 
3
3
  Given/When/Then decision-model testing surface for kairos-es.
4
4
 
5
- A `DeciderSpecification`-equivalent over the codec's `buildDecisionModel`, running
6
- the real read → decode → decide → append cycle against the in-memory store rather
7
- than a mock, on `@effect/vitest`. `@kairos-es/core`, `@kairos-es/codec`, `effect`
8
- and `@effect/vitest` are peer dependencies.
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
@@ -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":";;;;;AA0BA,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":[]}
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 the real
13
- * `@kairos-es/codec` `buildDecisionModel` fold and a `DcbEventStore` (the
14
- * in-memory layer in practice). It is shaped so an Event Modeling
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 `decider`. The returned `given(...)` starts the
95
- * Given/When/Then chain; the terminal step yields an `Effect` requiring
96
- * `DcbEventStore | Serializer | Uuid | R | RequirementOf<P>`.
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 specificationFor = decider => ({
113
+ const specificationForCycle = run => ({
99
114
  given: (...pastEvents) => ({
100
115
  when: command => {
101
- // Run the seed + decider cycle, exposing the outcome as an Exit so both
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 decider's outcome, so `then` reads strictly after that boundary
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((0, _codec.runDecider)(decider, command)), _effect.Effect.exit);
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","specificationFor","decider","given","pastEvents","when","command","runToExit","zip","runDecider","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","scenario","description","build","it","effect","provide","Layer","mergeAll","fresh","DcbEventStoreInMemory","SerializerDefault","UuidTest"],"sources":["../../src/specification.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
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":[]}
@@ -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 the real `@kairos-es/codec`
5
- * `buildDecisionModel` fold and a `DcbEventStore` (in practice the in-memory
6
- * layer). Shaped so an Event Modeling `Specification` (given events / when a
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) =>
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,cAAc,iBAAiB,CAAA"}
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 | CodecError | AppendConditionFailed` because a genuinely
45
- * failing cycle re-raises its cause (a `.then` on a failing decision is a
46
- * test-authoring mistake, made loud rather than silently green).
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 | CodecError | AppendConditionFailed, DcbEventStore | Serializer | Uuid | R | PR>;
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 | CodecError | AppendConditionFailed, DcbEventStore | Serializer | Uuid>) => void;
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":"AAqCA,OAAO,KAAK,EACV,UAAU,EACV,OAAO,EACP,aAAa,EACb,eAAe,EAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAEL,KAAK,UAAU,EAEf,KAAK,IAAI,EAEV,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EACb,aAAa,EAMd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAgB,MAAM,EAAuB,MAAM,QAAQ,CAAA;AAElE,mFAAmF;AACnF,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,CAC1C,QAAQ,EACR,UAAU,EACV,UAAU,GAAG,IAAI,GAAG,CAAC,CACtB,CAAA;AA+DD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,aAAa,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;IAC9C;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,GAAG,UAAU,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,KAC3C,SAAS,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;CAClC;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;IAC1C,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;CACxD;AAED,2EAA2E;AAC3E,MAAM,WAAW,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,CACb,GAAG,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,KACzC,MAAM,CAAC,MAAM,CAChB,IAAI,EACJ,CAAC,GAAG,UAAU,GAAG,qBAAqB,EACtC,aAAa,GAAG,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,CAC3C,CAAA;IACD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAClB,GAAG,EAAE,MAAM,KACR,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,aAAa,GAAG,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC,CAAA;CAC5E;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,CAAC,EACD,CAAC,EAED,SAAS,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,KACjC,aAAa,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,CAkD9C,CAAA;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC,EAC5E,aAAa,MAAM,EACnB,SAAS,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,EAC/B,OAAO,CACL,IAAI,EAAE,aAAa,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,KACrD,MAAM,CAAC,MAAM,CAChB,IAAI,EACJ,CAAC,GAAG,UAAU,GAAG,qBAAqB,EACtC,aAAa,GAAG,UAAU,GAAG,IAAI,CAClC,KACA,IAcF,CAAA"}
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 the real `@kairos-es/codec`
5
- * `buildDecisionModel` fold and a `DcbEventStore` (in practice the in-memory
6
- * layer). Shaped so an Event Modeling `Specification` (given events / when a
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) =>
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,cAAc,oBAAiB","ignoreList":[]}
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 the real
3
- * `@kairos-es/codec` `buildDecisionModel` fold and a `DcbEventStore` (the
4
- * in-memory layer in practice). It is shaped so an Event Modeling
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 `decider`. The returned `given(...)` starts the
88
- * Given/When/Then chain; the terminal step yields an `Effect` requiring
89
- * `DcbEventStore | Serializer | Uuid | R | RequirementOf<P>`.
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 specificationFor = decider => ({
106
+ export const specificationForCycle = run => ({
92
107
  given: (...pastEvents) => ({
93
108
  when: command => {
94
- // Run the seed + decider cycle, exposing the outcome as an Exit so both
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 decider's outcome, so `then` reads strictly after that boundary
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(runDecider(decider, command)), Effect.exit);
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","specificationFor","decider","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","scenario","description","build","effect","provide","mergeAll","fresh"],"sources":["../../src/specification.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
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.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.0",
17
- "@kairos-es/core": "^0.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 the real `@kairos-es/codec`
5
- * `buildDecisionModel` fold and a `DcbEventStore` (in practice the in-memory
6
- * layer). Shaped so an Event Modeling `Specification` (given events / when a
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) =>
@@ -1,7 +1,12 @@
1
1
  /**
2
- * The Given/When/Then surface: a `DeciderSpecification`-equivalent over the real
3
- * `@kairos-es/codec` `buildDecisionModel` fold and a `DcbEventStore` (the
4
- * in-memory layer in practice). It is shaped so an Event Modeling
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 | CodecError | AppendConditionFailed` because a genuinely
173
- * failing cycle re-raises its cause (a `.then` on a failing decision is a
174
- * test-authoring mistake, made loud rather than silently green).
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 | CodecError | AppendConditionFailed,
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 `decider`. The returned `given(...)` starts the
195
- * Given/When/Then chain; the terminal step yields an `Effect` requiring
196
- * `DcbEventStore | Serializer | Uuid | R | RequirementOf<P>`.
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 specificationFor = <
199
- P extends Record<string, SliceProjection>,
200
- Command,
201
- E,
202
- R,
203
- >(
204
- decider: Decider<P, Command, E, R>,
205
- ): Specification<Command, E, R, RequirementOf<P>> => ({
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 + decider cycle, exposing the outcome as an Exit so both
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 decider's outcome, so `then` reads strictly after that boundary
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(runDecider(decider, command)),
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(