@kairos-es/testing 1.0.0-alpha.1 → 1.0.0-alpha.2

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
@@ -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
@@ -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":";;;;;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":[]}
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","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
+ {"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":[]}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,cAAc,iBAAiB,CAAA"}
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>(decider: Decider<P, Command, E, R>) => Specification<Command, E, R, RequirementOf<P>>;
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>(description: string, decider: Decider<P, Command, E>, build: (spec: Specification<Command, E, never, RequirementOf<P>>) => Effect.Effect<void, E | CycleError, DcbEventStore | Serializer | Uuid>) => void;
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":"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"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,cAAc,oBAAiB","ignoreList":[]}
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","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":[]}
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.1",
3
+ "version": "1.0.0-alpha.2",
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.1",
17
- "@kairos-es/core": "^1.0.0-alpha.1"
16
+ "@kairos-es/codec": "^1.0.0-alpha.2",
17
+ "@kairos-es/core": "^1.0.0-alpha.2"
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
@@ -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 { Cause, Chunk, Effect, Exit, Layer, Stream } from 'effect'
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 = <P extends Record<string, SliceProjection>, Command, E>(
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>,