@shivaedev/test-story 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +248 -2
  4. package/dist/genreTags.d.ts +6 -0
  5. package/dist/genreTags.d.ts.map +1 -0
  6. package/dist/genreTags.js +5 -0
  7. package/dist/genreTags.js.map +1 -0
  8. package/dist/internal/callSite.d.ts +3 -0
  9. package/dist/internal/callSite.d.ts.map +1 -0
  10. package/dist/internal/callSite.js +16 -0
  11. package/dist/internal/callSite.js.map +1 -0
  12. package/dist/internal/engineSnapshot.d.ts +4 -0
  13. package/dist/internal/engineSnapshot.d.ts.map +1 -0
  14. package/dist/internal/engineSnapshot.js +35 -0
  15. package/dist/internal/engineSnapshot.js.map +1 -0
  16. package/dist/internal/errors.d.ts +5 -0
  17. package/dist/internal/errors.d.ts.map +1 -0
  18. package/dist/internal/errors.js +15 -0
  19. package/dist/internal/errors.js.map +1 -0
  20. package/dist/internal/failureReport.d.ts +8 -0
  21. package/dist/internal/failureReport.d.ts.map +1 -0
  22. package/dist/internal/failureReport.js +40 -0
  23. package/dist/internal/failureReport.js.map +1 -0
  24. package/dist/internal/genre.d.ts +7 -0
  25. package/dist/internal/genre.d.ts.map +1 -0
  26. package/dist/internal/genre.js +8 -0
  27. package/dist/internal/genre.js.map +1 -0
  28. package/dist/internal/hooked.d.ts +7 -0
  29. package/dist/internal/hooked.d.ts.map +1 -0
  30. package/dist/internal/hooked.js +11 -0
  31. package/dist/internal/hooked.js.map +1 -0
  32. package/dist/internal/narration.d.ts +25 -0
  33. package/dist/internal/narration.d.ts.map +1 -0
  34. package/dist/internal/narration.js +34 -0
  35. package/dist/internal/narration.js.map +1 -0
  36. package/dist/internal/render.d.ts +8 -0
  37. package/dist/internal/render.d.ts.map +1 -0
  38. package/dist/internal/render.js +56 -0
  39. package/dist/internal/render.js.map +1 -0
  40. package/dist/internal/runUntil.d.ts +17 -0
  41. package/dist/internal/runUntil.d.ts.map +1 -0
  42. package/dist/internal/runUntil.js +24 -0
  43. package/dist/internal/runUntil.js.map +1 -0
  44. package/dist/internal/seed.d.ts +26 -0
  45. package/dist/internal/seed.d.ts.map +1 -0
  46. package/dist/internal/seed.js +27 -0
  47. package/dist/internal/seed.js.map +1 -0
  48. package/dist/internal/start.d.ts +7 -0
  49. package/dist/internal/start.d.ts.map +1 -0
  50. package/dist/internal/start.js +23 -0
  51. package/dist/internal/start.js.map +1 -0
  52. package/dist/internal/storyIt.d.ts +16 -0
  53. package/dist/internal/storyIt.d.ts.map +1 -0
  54. package/dist/internal/storyIt.js +46 -0
  55. package/dist/internal/storyIt.js.map +1 -0
  56. package/dist/internal/traits.d.ts +12 -0
  57. package/dist/internal/traits.d.ts.map +1 -0
  58. package/dist/internal/traits.js +8 -0
  59. package/dist/internal/traits.js.map +1 -0
  60. package/dist/internal/wrap.d.ts +8 -0
  61. package/dist/internal/wrap.d.ts.map +1 -0
  62. package/dist/internal/wrap.js +49 -0
  63. package/dist/internal/wrap.js.map +1 -0
  64. package/dist/storyKit.d.ts +65 -0
  65. package/dist/storyKit.d.ts.map +1 -0
  66. package/dist/storyKit.js +11 -0
  67. package/dist/storyKit.js.map +1 -0
  68. package/docs/north-star.md +35 -0
  69. package/docs/roadmap.md +18 -0
  70. package/package.json +72 -3
  71. package/src/genreTags.ts +10 -0
  72. package/src/internal/callSite.ts +18 -0
  73. package/src/internal/engineSnapshot.ts +47 -0
  74. package/src/internal/errors.ts +39 -0
  75. package/src/internal/failureReport.ts +54 -0
  76. package/src/internal/genre.ts +14 -0
  77. package/src/internal/hooked.ts +22 -0
  78. package/src/internal/narration.ts +57 -0
  79. package/src/internal/render.ts +69 -0
  80. package/src/internal/runUntil.ts +43 -0
  81. package/src/internal/seed.ts +53 -0
  82. package/src/internal/start.ts +24 -0
  83. package/src/internal/storyIt.ts +63 -0
  84. package/src/internal/traits.ts +20 -0
  85. package/src/internal/wrap.ts +50 -0
  86. package/src/storyKit.ts +126 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-10-06
4
+
5
+ ### Added
6
+
7
+ - `@shivaedev/test-story/storyKit.ts` exports `storyKit(definition)`, one story kit per engine. The definition names the engine, says how to `create` a fresh one, the optional `layer` that provides its services, the `stages` traits are applied in with an optional `after` hook per stage, the optional `run` hooks (`step`, `maxSteps`, `failed`, `diagnose`), an optional `inspect` and the `verbs` a test acts through. Every hook may be a plain function or a generator function that yields Effects, and every type is inferred from the definition. A hook, trait or test body that needs a service the layer does not provide does not compile.
8
+ - The kit returns `trait`, `traits` and its own `it(name, traits, body, options)`, built on `@shivaedev/effect-test`. Each test starts a fresh engine, seeds the traits stage by stage and runs the body with the verbs and the `story`: its `engine`, `lines`, `tell(line)` and `runUntil(condition, { maxSteps })`. `it.fails`, `it.only`, `it.skip`, `it.runIf` and `it.skipIf` work as in Vitest.
9
+ - Every test of a kit is named after its genre, such as `Bakery Story: steps until the condition holds`, so `-t "Bakery Story: "` picks one engine's stories. `@shivaedev/test-story/genreTags.ts` exports `genreTags(...kitNames)`, which declares a Vitest tag per kit, such as `bakery-story`, for `--tags-filter`. A kit tags its tests only with a genre the config declares, and runs them untagged and silently otherwise.
10
+ - A trait that throws, fails or dies refuses the setup with `the trait "<line>" refused to set up the <engine>: <cause>`, a `help:` line and the thrown value as the error's `cause`. An interrupted trait stays interrupted.
11
+ - `story.runUntil` steps the engine until the condition holds, dies with what `run.failed` reports even when the condition also holds, and after `maxSteps` steps dies with `run.diagnose`. The typed failures of the run hooks and the condition stay in its error channel.
12
+ - A failed story prints under its test's first error: a short guide to reading it, every line in order, a mark where it stopped or on the trait that refused with the spec line there, the engine's state from `inspect` as JSON (written to `node_modules/.cache/test-story/` when longer than 2,000 characters), and the command that reruns the test. The package breaks what it prints at 120 columns, except the story's own lines. A thrown string, plain object or tagged error prints by its content.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShivaeDev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,249 @@
1
- # Temporary Holding Version
1
+ # @shivaedev/test-story
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Tests that read like short English stories and run the real engine. A spec names its setup in domain words, acts through the application's verbs and lets the engine run, so a human sees at a glance what is tested and an agent writes fewer lines to test more of the stack.
4
+
5
+ ## Why you want this
6
+
7
+ When agents write most of the code, test files grow into hundreds of lines nobody reads. Each test rebuilds its setup by hand, mocks whatever was too much work to build, and hides what it actually checks. A story test holds only what is specific to it:
8
+
9
+ ```ts
10
+ bakery.it("steps until the condition holds", [ovenIsLit(), hasDough(3)], function* ({ oven }) {
11
+ expect(yield* oven.bakesEverything()).toEqual({ loaves: 3, minutes: 3 });
12
+ });
13
+ ```
14
+
15
+ That is a whole test. `bakery.it` comes from the bakery's story kit. The list holds its traits, `ovenIsLit()` and `hasDough(3)`, which are written once and shared by every spec. `oven.bakesEverything()` is a verb that runs the bakery's real code until the dough is gone. Every story runs the whole engine, so each test also checks the parts its author never thought to mock, and because a spec reads as English, a sloppy test stands out in review.
16
+
17
+ When a story fails, it prints itself. One of this package's own failing stories expects 2 loaves from one ball of dough, and the test fails with this message:
18
+
19
+ ```text
20
+ loaves: expected 1 to be 2 // Object.is equality
21
+
22
+ ╭─ test-story: how to read the story below
23
+ │ "given" lines are the test's traits, the other lines were told by verbs and engine steps as they ran, and ✗ marks
24
+ │ where the test stopped. The traits, verbs and steps live in the bakery story kit that this test imports.
25
+ ╰─
26
+
27
+ given the oven is lit
28
+ given the baker has 1 balls of dough
29
+ 1m a loaf comes out of the oven
30
+ ✗ the test failed after the line above
31
+ at src/test-support/failingStories.ts:10:59
32
+
33
+ The bakery when the test failed:
34
+ {"bowls":1,"capacity":3,"dough":0,"flour":0,"loaves":1,"minute":1,"orders":[],"ovenLit":true,"overfired":false,
35
+ "smoking":false,"starter":false}
36
+
37
+ Rerun: vitest run src/test-support/failingStories.ts -t "Bakery Story: marks where an assertion stopped the story"
38
+ ```
39
+
40
+ The first line is the assertion's own message. The box under it tells a reader who has never seen the package how to read what follows. Then comes the story: the two traits the test named, the line the bakery told as it baked, and a `✗` where the test stopped, with the spec line it stopped at. Last come the bakery's whole state at that moment and the command that reruns the test, so the failure alone is enough to find the cause.
41
+
42
+ ## Using it
43
+
44
+ ### How to think about it
45
+
46
+ An application has an **engine**: the real thing a test drives, such as a bakery simulation, a game store or a service with its database. A **story** is one test's run of a fresh engine. It starts from **traits**, sentences of setup in domain words that each change the engine, and goes on through **verbs**, the entry points a user or caller reaches, such as `baker.kneads(2)` or `oven.bakesEverything()`. The engine may also run on its own, one **step** at a time, until something the test waits for holds. Every trait tells a line, and verbs and steps tell what they do, so the story is both the plan the test gave and the trace of what happened, in order.
47
+
48
+ Every story belongs to a **genre**, the kind of engine it runs. The kit names each of its tests after its genre, such as `Bakery Story: steps until the condition holds`, so a report shows which engine a test runs and one run can pick the bakery's stories and leave the others.
49
+
50
+ The package runs on Effect and Vitest, but an engine needs neither: every hook, trait and test body may be a plain function. A hook that needs a service or fails with a typed error is a generator function that yields Effects, and the kit runs it, so no definition writes `Effect.gen`.
51
+
52
+ The work splits into three layers, and each is written by a different hand at a different time:
53
+
54
+ 1. **Once per engine**, a story kit in `test-support/` says how to create the engine, which services it needs, and how to seed, step and show it.
55
+ 2. **Once per feature**, traits and verbs in the kit name that feature in domain words.
56
+ 3. **In every test**, a spec tells a story with the kit's `it` and checks what a user would see.
57
+
58
+ ### 1. Once per engine: the story kit
59
+
60
+ `storyKit(definition)` from `@shivaedev/test-story/storyKit.ts` turns one definition into the kit. Every type, the engine's, the stages', the verbs' and the services', is inferred from it.
61
+
62
+ ```ts
63
+ export const bakery = storyKit({
64
+ after: {
65
+ *kitchen(state) {
66
+ if (state.bowls === 0) {
67
+ return yield* new NoBowls();
68
+ }
69
+ state.capacity = state.bowls * DOUGH_PER_BOWL;
70
+ },
71
+ },
72
+ create: emptyBakery,
73
+ layer: Supplier.layer,
74
+ name: "bakery",
75
+ run: {
76
+ diagnose: stillWaiting,
77
+ failed: ovenTrouble,
78
+ maxSteps: 60,
79
+ *step(state, tell) {
80
+ state.minute += 1;
81
+ if (state.overfired) {
82
+ return yield* new BurntLoaf({ minute: state.minute });
83
+ }
84
+ state.dough += state.starter ? 0 : -1;
85
+ state.loaves += 1;
86
+ tell(`${state.minute}m a loaf comes out of the oven`);
87
+ },
88
+ },
89
+ stages: ["kitchen", "pantry"],
90
+ verbs: (state, story) => ({
91
+ baker: {
92
+ kneads: (count: number) => {
93
+ story.tell(`the baker kneads ${count} balls of dough`);
94
+ state.dough += count;
95
+ },
96
+ },
97
+ oven: {
98
+ bakesEverything: (maxSteps?: number): Effect.Effect<BakeReport, BurntLoaf> =>
99
+ story.runUntil((current) => current.dough === 0, { maxSteps }).pipe(Effect.map(() => ({ loaves: state.loaves, minutes: state.minute }))),
100
+ },
101
+ }),
102
+ });
103
+ ```
104
+
105
+ This is the bakery kit this package tests itself with. `emptyBakery`, `stillWaiting` and `ovenTrouble` are plain functions beside it. `kitchen` and `step` are generators because they fail with typed errors, `NoBowls` and `BurntLoaf`.
106
+
107
+ | Hook | Says |
108
+ | --- | --- |
109
+ | `name` | What the engine is called in failures, "the bakery", and the kit's genre, "Bakery Story". |
110
+ | `create()` | A fresh engine. Every story starts from a new one. |
111
+ | `layer` | Optional. The Effect layer that provides every service the hooks, traits and test bodies use. A hook that needs a service the layer does not provide does not compile, and neither does a test whose traits or body need one. |
112
+ | `stages` | The order traits are applied in. A trait names its stage, and one that names a stage the kit does not declare does not compile. |
113
+ | `after` | Optional. A hook per stage that runs once that stage's traits are applied, even when no trait names the stage, so a value it derives is ready before a later stage reads it. Its typed failure fails the test. |
114
+ | `run` | Optional. What `story.runUntil` needs: `step(engine, tell)` advances the engine once and tells what happened, `maxSteps` caps one run, `failed(engine)` returns a sentence when the engine is in a state it cannot recover from, and `diagnose(engine)` says why it has not finished. |
115
+ | `inspect` | Optional. The part of the engine a failure prints. Without it, the failure prints the whole engine. |
116
+ | `verbs(engine, story)` | The entry points a test acts through. A verb that runs the engine returns the Effect from `story.runUntil`. |
117
+
118
+ Every test of the kit is named `Bakery Story: <its name>`, so `vitest run -t "Bakery Story: "` runs only the bakery's stories with no setup. A project that splits runs by Vitest tags can also tag them:
119
+
120
+ ```ts
121
+ import { genreTags } from "@shivaedev/test-story/genreTags.ts";
122
+ import { defineConfig } from "vitest/config";
123
+
124
+ export default defineConfig({
125
+ test: { tags: genreTags("bakery", "mill") },
126
+ });
127
+ ```
128
+
129
+ `genreTags(...kitNames)` declares one tag per kit, named after it: `bakery-story` and `mill-story` here, and `game-store-story` for a kit named "game store". A kit tags its tests only with a genre the config declares, because Vitest refuses a test whose tag the config leaves out, so a config without the line runs every story untagged and prints nothing about it. With the tags declared, `vitest run --tags-filter=mill-story` runs the mill's stories and skips the bakery's, and `--tags-filter` takes any of Vitest's tag expressions, such as `'!mill-story'`. A root config that runs the package's projects by extending its config gets these tags through [`inheritTags`](https://github.com/ShivaeDev/platform/tree/main/packages/quality#inherited-tags) from `@shivaedev/quality`, because Vitest does not pass an extended config's tags on.
130
+
131
+ ### 2. Once per feature: traits and verbs
132
+
133
+ A trait is a line and a change, tagged with a stage:
134
+
135
+ ```ts
136
+ export function hasDough(count: number) {
137
+ return bakery.trait("pantry", `the baker has ${count} balls of dough`, (state) => {
138
+ if (count > state.capacity) {
139
+ throw new Error(`the bowls hold only ${state.capacity}`);
140
+ }
141
+ state.dough = count;
142
+ });
143
+ }
144
+
145
+ export function hasFlourDelivered(sacks: number) {
146
+ return bakery.trait("pantry", `the supplier has delivered ${sacks} sacks of flour`, function* (state) {
147
+ const supplier = yield* Supplier;
148
+ yield* supplier.deliver(sacks);
149
+ state.flour = sacks;
150
+ });
151
+ }
152
+
153
+ export function morningShift() {
154
+ return bakery.traits(ovenIsLit(), hasBowls(1));
155
+ }
156
+ ```
157
+
158
+ `hasDough` is a plain function. `hasFlourDelivered` is a generator because it reaches the `Supplier` service, which the kit's layer provides. `traits(...)` bundles traits into one, so a setup that recurs gets one name. A test may name traits in any order: `[hasDough(6), hasBowls(2)]` fills the bowls only after the kitchen has them.
159
+
160
+ A trait that throws, fails or dies refuses the setup instead of seeding a state the application could never reach. This package's failing story `bakery.it("marks the trait that refused", [ovenIsLit(), hasDough(4), hasBowls(1)])` asks for more dough than one bowl holds and fails with:
161
+
162
+ ```text
163
+ the trait "the baker has 4 balls of dough" refused to set up the bakery: the bowls hold only 3
164
+ help: a trait throws when the bakery it asks for cannot exist. Give the test traits that fit together, or fix the trait
165
+ in the bakery story kit if this bakery should be possible.
166
+
167
+ ╭─ test-story: how to read the story below
168
+ │ "given" lines are the test's traits, the other lines were told by verbs and engine steps as they ran, and ✗ marks
169
+ │ where the test stopped. The traits, verbs and steps live in the bakery story kit that this test imports.
170
+ ╰─
171
+
172
+ given the oven is lit
173
+ ✗ given the baker has 4 balls of dough
174
+ refused at src/test-support/failingStories.ts:13:57
175
+ given the bakery has 1 bowls
176
+
177
+ The bakery when the test failed:
178
+ {"bowls":1,"capacity":3,"dough":0,"flour":0,"loaves":0,"minute":0,"orders":[],"ovenLit":true,"overfired":false,
179
+ "smoking":false,"starter":false}
180
+
181
+ Rerun: vitest run src/test-support/failingStories.ts -t "Bakery Story: marks the trait that refused"
182
+ ```
183
+
184
+ The first line names the trait and what it threw, which the error also keeps as its `cause`, and the `help:` line says what to change. The `✗` marks the trait that refused, and the line under it points at the spec line that named it. A trait that fails with a string, a plain object or a tagged error prints by its content, never as `[object Object]`, and an interrupted trait stays interrupted.
185
+
186
+ A verb that a feature adds goes into the kit's `verbs`. It acts through the engine's real entry point and tells a line, so the story shows it.
187
+
188
+ ### 3. In every test: a spec
189
+
190
+ ```ts
191
+ bakery.it("tells each step into the story", [ovenIsLit()], function* ({ baker, oven, story }) {
192
+ baker.kneads(2);
193
+ yield* oven.bakesEverything();
194
+
195
+ expect(story.lines).toEqual([
196
+ "the oven is lit",
197
+ "the baker kneads 2 balls of dough",
198
+ "1m a loaf comes out of the oven",
199
+ "2m a loaf comes out of the oven",
200
+ ]);
201
+ });
202
+ ```
203
+
204
+ `kit.it(name, traits, body, options)` declares one Vitest test, named after the kit's genre: `Bakery Story: tells each step into the story`. It starts a fresh engine, seeds the traits stage by stage and runs the body with the kit's verbs and the `story`: its `engine`, `lines`, `tell(line)` and `runUntil`. The body's second argument is Vitest's test context. A body that yields Effects, such as a verb that runs the engine, is a generator, and one that yields nothing is a plain function. A test that only checks that its traits refuse may leave the body out. `options` is a timeout in milliseconds or Vitest's test options, whose own `tags` join the genre tag when the config declares it. `it.fails`, `it.only`, `it.skip`, `it.runIf(condition)` and `it.skipIf(condition)` work as they do in Vitest.
205
+
206
+ A spec starts a story, acts through verbs and checks what a user would see. When it needs a setup or an action the kit lacks, add a trait or verb to the kit instead of building it in the spec.
207
+
208
+ ### Running the engine
209
+
210
+ `story.runUntil(condition, { maxSteps })` returns an Effect that asks three questions before every step:
211
+
212
+ 1. `run.failed` returns a sentence: it dies with it, even when the condition also holds.
213
+ 2. The condition holds: it succeeds.
214
+ 3. `maxSteps` steps have run, or the kit's `run.maxSteps` when the call names none: it dies with `run.diagnose`.
215
+
216
+ ```text
217
+ the bakery ran 12 steps and never reached what runUntil waits for: 1 balls of dough still wait after 12 minutes: the
218
+ sourdough starter never runs out
219
+ help: either the bakery never gets there, so check the setup and the engine, or it needs more steps, so pass a larger
220
+ maxSteps to runUntil.
221
+ ```
222
+
223
+ That is what `oven.bakesEverything(12)` dies with when the baker keeps a sourdough starter, so the dough never runs out. The typed failures of `step`, `failed`, `diagnose` and the condition stay in the Effect's error channel: a step that fails with `BurntLoaf` gives the test a `BurntLoaf` to assert on. A kit without `run` hooks dies with a message that says to add them.
224
+
225
+ ### When a story fails
226
+
227
+ When a story's test fails, for any reason, the story is added to its first error, as in the examples above:
228
+
229
+ - a short guide to reading the story, in a box;
230
+ - every line in order, traits marked `given`;
231
+ - `✗` after the last line, on the trait that refused, or alone when the test failed before the story told a line, and under it the spec line where the test stopped or that named the refusing trait;
232
+ - the engine's state from `inspect` as JSON, broken between its fields to fit 120 columns. State longer than 2,000 characters is written to `node_modules/.cache/test-story/` under the working directory, and the failure names the file;
233
+ - the command that reruns the test, with `-t` on a second line when one line would pass 120 columns.
234
+
235
+ The package breaks what it prints at 120 columns, between words or JSON fields, and prints the story's own lines whole. A passing test adds nothing. A typed failure with no message, such as an `after` hook's `NoBowls`, prints by its tag.
236
+
237
+ ### Install and limits
238
+
239
+ ```sh
240
+ pnpm add --save-dev @shivaedev/test-story @effect/vitest effect vitest
241
+ ```
242
+
243
+ `@effect/vitest`, `effect` and `vitest` are peers, so the application's own versions run the stories.
244
+
245
+ - The package runs inside Vitest. The story reaches the failure by rewriting the first error's message in Vitest's `onTestFailed` hook.
246
+ - A kit's `it` is created when the kit is defined, so a kit lives in a module that only Vitest test files import.
247
+ - `inspect` runs after the test has failed, so it reads the engine synchronously.
248
+ - The spec line under the `✗` is found by matching the spec's path in the failure's stack, or else in the call site of the last line the story told or the last `runUntil`. A failure raised inside an Effect that the spec does not call directly may have none.
249
+ - The package is built for ShivaeDev applications and may change without a deprecation period.
@@ -0,0 +1,6 @@
1
+ export interface GenreTag {
2
+ readonly description: string;
3
+ readonly name: string;
4
+ }
5
+ export declare function genreTags(...kitNames: readonly string[]): GenreTag[];
6
+ //# sourceMappingURL=genreTags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genreTags.d.ts","sourceRoot":"","sources":["../src/genreTags.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,SAAS,CAAC,GAAG,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,EAAE,CAEpE"}
@@ -0,0 +1,5 @@
1
+ import { genreOf } from "#internal/genre.ts";
2
+ export function genreTags(...kitNames) {
3
+ return kitNames.map((kitName) => genreOf(kitName).tag);
4
+ }
5
+ //# sourceMappingURL=genreTags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genreTags.js","sourceRoot":"","sources":["../src/genreTags.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAO7C,MAAM,UAAU,SAAS,CAAC,GAAG,QAA2B;IACvD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function callSite(): Error;
2
+ export declare function locate(stack: string | undefined, testFile: string): string | undefined;
3
+ //# sourceMappingURL=callSite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callSite.d.ts","sourceRoot":"","sources":["../../src/internal/callSite.ts"],"names":[],"mappings":"AAKA,wBAAgB,QAAQ,IAAI,KAAK,CAMhC;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAItF"}
@@ -0,0 +1,16 @@
1
+ import { relative } from "node:path";
2
+ const FRAMES = 50;
3
+ // V8 formats a stack only when it is read, so a site costs little until a test fails.
4
+ export function callSite() {
5
+ const limit = Error.stackTraceLimit;
6
+ Error.stackTraceLimit = FRAMES;
7
+ const site = new Error("call site");
8
+ Error.stackTraceLimit = limit;
9
+ return site;
10
+ }
11
+ export function locate(stack, testFile) {
12
+ const frame = stack?.split("\n").find((line) => line.includes(testFile));
13
+ const position = frame?.slice(frame.indexOf(testFile) + testFile.length).match(/^:(?<line>\d+):(?<column>\d+)/u)?.groups;
14
+ return position === undefined ? undefined : `${relative(process.cwd(), testFile)}:${position.line}:${position.column}`;
15
+ }
16
+ //# sourceMappingURL=callSite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callSite.js","sourceRoot":"","sources":["../../src/internal/callSite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,MAAM,MAAM,GAAG,EAAE,CAAC;AAElB,sFAAsF;AACtF,MAAM,UAAU,QAAQ;IACvB,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,CAAC;IACpC,KAAK,CAAC,eAAe,GAAG,MAAM,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC;IACpC,KAAK,CAAC,eAAe,GAAG,KAAK,CAAC;IAC9B,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAyB,EAAE,QAAgB;IACjE,MAAM,KAAK,GAAG,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,gCAAgC,CAAC,EAAE,MAAM,CAAC;IACzH,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,IAAI,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;AACxH,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { FailedTest } from "#internal/failureReport.ts";
2
+ import type { Subject } from "#internal/narration.ts";
3
+ export declare function engineSnapshot<TEngine>(subject: Subject<TEngine>, test: FailedTest): string[];
4
+ //# sourceMappingURL=engineSnapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engineSnapshot.d.ts","sourceRoot":"","sources":["../../src/internal/engineSnapshot.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAuBtD,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,CAoB7F"}
@@ -0,0 +1,35 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import { describeValue, toJson } from "#internal/render.ts";
4
+ import { WIDTH, wrap, wrapJson } from "#internal/wrap.ts";
5
+ const INLINE_CHARACTERS = 2000;
6
+ function snapshot(subject) {
7
+ try {
8
+ return { value: subject.inspect === undefined ? subject.engine : subject.inspect(subject.engine) };
9
+ }
10
+ catch (threw) {
11
+ return { threw };
12
+ }
13
+ }
14
+ function slug(text) {
15
+ return text
16
+ .toLowerCase()
17
+ .replaceAll(/[^a-z0-9.]+/gu, "-")
18
+ .slice(0, 80);
19
+ }
20
+ export function engineSnapshot(subject, test) {
21
+ const heading = `The ${subject.name} when the test failed`;
22
+ const taken = snapshot(subject);
23
+ if ("threw" in taken) {
24
+ return wrap(`${heading} is unknown: its inspect hook threw ${describeValue(taken.threw)}`);
25
+ }
26
+ const json = toJson(taken.value);
27
+ if (json.length <= INLINE_CHARACTERS) {
28
+ return heading.length + 2 + json.length > WIDTH ? [`${heading}:`, ...wrapJson(json)] : [`${heading}: ${json}`];
29
+ }
30
+ const file = join(process.cwd(), "node_modules", ".cache", "test-story", `${slug(basename(test.file))}--${slug(test.name)}--${slug(subject.name)}.json`);
31
+ mkdirSync(dirname(file), { recursive: true });
32
+ writeFileSync(file, toJson(taken.value, 2));
33
+ return wrap(`${heading} is ${json.length} characters of JSON, too long to print here. Read it in ${relative(process.cwd(), file)}`);
34
+ }
35
+ //# sourceMappingURL=engineSnapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engineSnapshot.js","sourceRoot":"","sources":["../../src/internal/engineSnapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAG9D,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAI/B,SAAS,QAAQ,CAAU,OAAyB;IACnD,IAAI,CAAC;QACJ,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACpG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,EAAE,KAAK,EAAE,CAAC;IAClB,CAAC;AACF,CAAC;AAED,SAAS,IAAI,CAAC,IAAY;IACzB,OAAO,IAAI;SACT,WAAW,EAAE;SACb,UAAU,CAAC,eAAe,EAAE,GAAG,CAAC;SAChC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,cAAc,CAAU,OAAyB,EAAE,IAAgB;IAClF,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,uBAAuB,CAAC;IAC3D,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC,GAAG,OAAO,uCAAuC,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC5F,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,IAAI,CAAC,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACtC,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,IAAI,EAAE,CAAC,CAAC;IAChH,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAChB,OAAO,CAAC,GAAG,EAAE,EACb,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAC9E,CAAC;IACF,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,IAAI,CAAC,GAAG,OAAO,OAAO,IAAI,CAAC,MAAM,2DAA2D,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;AACrI,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare function refusal(name: string, line: string, cause: unknown): Error;
2
+ export declare function broken(name: string, steps: number, reason: string): Error;
3
+ export declare function unfinished(name: string, steps: number, diagnosis: string | undefined): Error;
4
+ export declare function cannotRun(name: string): Error;
5
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAGA,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,CAQzE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAOzE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,CAO5F;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAO7C"}
@@ -0,0 +1,15 @@
1
+ import { describeValue } from "#internal/render.ts";
2
+ import { explained } from "#internal/wrap.ts";
3
+ export function refusal(name, line, cause) {
4
+ return new Error(explained(`the trait "${line}" refused to set up the ${name}: ${describeValue(cause)}`, `a trait throws when the ${name} it asks for cannot exist. Give the test traits that fit together, or fix the trait in the ${name} story kit if this ${name} should be possible.`), { cause });
5
+ }
6
+ export function broken(name, steps, reason) {
7
+ return new Error(explained(`the ${name} broke after ${steps} steps: ${reason}`, `run.failed in the ${name} story kit reports a state the ${name} cannot recover from. The story printed with this failure shows the setup and steps that led here.`));
8
+ }
9
+ export function unfinished(name, steps, diagnosis) {
10
+ return new Error(explained(`the ${name} ran ${steps} steps and never reached what runUntil waits for${diagnosis === undefined ? "" : `: ${diagnosis}`}`, `either the ${name} never gets there, so check the setup and the engine, or it needs more steps, so pass a larger maxSteps to runUntil.`));
11
+ }
12
+ export function cannotRun(name) {
13
+ return new Error(explained(`the ${name} story kit has no run hooks, so runUntil cannot step the ${name}`, `give the ${name} story kit run: { maxSteps, step }, where step advances the ${name} by one step.`));
14
+ }
15
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,IAAY,EAAE,KAAc;IACjE,OAAO,IAAI,KAAK,CACf,SAAS,CACR,cAAc,IAAI,2BAA2B,IAAI,KAAK,aAAa,CAAC,KAAK,CAAC,EAAE,EAC5E,2BAA2B,IAAI,8FAA8F,IAAI,sBAAsB,IAAI,sBAAsB,CACjL,EACD,EAAE,KAAK,EAAE,CACT,CAAC;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,KAAa,EAAE,MAAc;IACjE,OAAO,IAAI,KAAK,CACf,SAAS,CACR,OAAO,IAAI,gBAAgB,KAAK,WAAW,MAAM,EAAE,EACnD,qBAAqB,IAAI,kCAAkC,IAAI,oGAAoG,CACnK,CACD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAa,EAAE,SAA6B;IACpF,OAAO,IAAI,KAAK,CACf,SAAS,CACR,OAAO,IAAI,QAAQ,KAAK,mDAAmD,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,EAAE,EAC5H,cAAc,IAAI,sHAAsH,CACxI,CACD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACrC,OAAO,IAAI,KAAK,CACf,SAAS,CACR,OAAO,IAAI,4DAA4D,IAAI,EAAE,EAC7E,YAAY,IAAI,+DAA+D,IAAI,eAAe,CAClG,CACD,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Narration, Subject } from "#internal/narration.ts";
2
+ export interface FailedTest {
3
+ readonly file: string;
4
+ readonly name: string;
5
+ readonly stack: string | undefined;
6
+ }
7
+ export declare function failureReport<TEngine>(narration: Narration, subject: Subject<TEngine>, test: FailedTest): string;
8
+ //# sourceMappingURL=failureReport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failureReport.d.ts","sourceRoot":"","sources":["../../src/internal/failureReport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAIjE,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAMhH"}
@@ -0,0 +1,40 @@
1
+ import { relative } from "node:path";
2
+ import { locate } from "#internal/callSite.ts";
3
+ import { engineSnapshot } from "#internal/engineSnapshot.ts";
4
+ import { describeValue } from "#internal/render.ts";
5
+ import { WIDTH, wrap } from "#internal/wrap.ts";
6
+ export function failureReport(narration, subject, test) {
7
+ try {
8
+ return [...guide(subject.name), "", ...storyRows(narration, test), "", ...engineSnapshot(subject, test), "", rerun(test)].join("\n");
9
+ }
10
+ catch (error) {
11
+ return `test-story could not print the story of this ${subject.name}: ${describeValue(error)}`;
12
+ }
13
+ }
14
+ function guide(name) {
15
+ return [
16
+ "╭─ test-story: how to read the story below",
17
+ ...wrap(`"given" lines are the test's traits, the other lines were told by verbs and engine steps as they ran, and ✗ marks where the test stopped. The traits, verbs and steps live in the ${name} story kit that this test imports.`, { first: "│ ", rest: "│ " }),
18
+ "╰─",
19
+ ];
20
+ }
21
+ function storyRows(narration, test) {
22
+ const rows = narration.lines.map((line, index) => `${narration.refused?.index === index ? "✗" : " "} ${index < narration.given ? "given" : " "} ${line}`);
23
+ if (narration.refused !== undefined) {
24
+ const where = locate(narration.sites[narration.refused.index]?.stack, test.file);
25
+ rows.splice(narration.refused.index + 1, 0, ...(where === undefined ? [] : [` refused at ${where}`]));
26
+ return rows;
27
+ }
28
+ const where = locate(test.stack, test.file) ?? locate(narration.at?.stack, test.file);
29
+ return [
30
+ ...rows,
31
+ `✗ the test failed ${rows.length === 0 ? "before the story told a line" : "after the line above"}`,
32
+ ...(where === undefined ? [] : [` at ${where}`]),
33
+ ];
34
+ }
35
+ function rerun(test) {
36
+ const run = `Rerun: vitest run ${relative(process.cwd(), test.file)}`;
37
+ const filter = `-t "${test.name.replaceAll(/[$()*+.?[\\\]^{|}]/gu, "\\$&")}"`;
38
+ return run.length + 1 + filter.length > WIDTH ? `${run} \\\n ${filter}` : `${run} ${filter}`;
39
+ }
40
+ //# sourceMappingURL=failureReport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failureReport.js","sourceRoot":"","sources":["../../src/internal/failureReport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { GenreTag } from "#genreTags.ts";
2
+ export interface Genre {
3
+ readonly tag: GenreTag;
4
+ readonly title: string;
5
+ }
6
+ export declare function genreOf(kitName: string): Genre;
7
+ //# sourceMappingURL=genre.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genre.d.ts","sourceRoot":"","sources":["../../src/internal/genre.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAM9C"}
@@ -0,0 +1,8 @@
1
+ export function genreOf(kitName) {
2
+ const words = kitName.trim().split(/\s+/u);
3
+ return {
4
+ tag: { description: `stories over a real ${words.join(" ")}`, name: [...words.map((word) => word.toLowerCase()), "story"].join("-") },
5
+ title: [...words, "story"].map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(" "),
6
+ };
7
+ }
8
+ //# sourceMappingURL=genre.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genre.js","sourceRoot":"","sources":["../../src/internal/genre.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,OAAO,CAAC,OAAe;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO;QACN,GAAG,EAAE,EAAE,WAAW,EAAE,uBAAuB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QACrI,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;KACrG,CAAC;AACH,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { Effect } from "effect";
2
+ export type AnyEffect = Effect.Effect<unknown, unknown, unknown>;
3
+ export type Hook<TArguments extends readonly unknown[], TEffect extends AnyEffect, A> = (...input: TArguments) => Generator<TEffect, A, never> | A;
4
+ export type ErrorOf<TEffect> = [TEffect] extends [never] ? never : [TEffect] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E : never;
5
+ export type ServicesOf<TEffect> = [TEffect] extends [never] ? never : [TEffect] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R : never;
6
+ export declare function hooked<TEffect extends AnyEffect, A>(call: () => Generator<TEffect, A, never> | A): Effect.Effect<A, ErrorOf<TEffect>, ServicesOf<TEffect>>;
7
+ //# sourceMappingURL=hooked.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooked.d.ts","sourceRoot":"","sources":["../../src/internal/hooked.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAEjE,MAAM,MAAM,IAAI,CAAC,UAAU,SAAS,SAAS,OAAO,EAAE,EAAE,OAAO,SAAS,SAAS,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,EAAE,UAAU,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;AAEnJ,MAAM,MAAM,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAE9I,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAMjJ,wBAAgB,MAAM,CAAC,OAAO,SAAS,SAAS,EAAE,CAAC,EAClD,IAAI,EAAE,MAAM,SAAS,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,GAC1C,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAKzD"}
@@ -0,0 +1,11 @@
1
+ import { Effect } from "effect";
2
+ function isGenerator(value) {
3
+ return typeof value === "object" && value !== null && Symbol.iterator in value && "next" in value && typeof value.next === "function";
4
+ }
5
+ export function hooked(call) {
6
+ return Effect.suspend(() => {
7
+ const result = call();
8
+ return isGenerator(result) ? Effect.gen(() => result) : Effect.succeed(result);
9
+ });
10
+ }
11
+ //# sourceMappingURL=hooked.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooked.js","sourceRoot":"","sources":["../../src/internal/hooked.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAUhC,SAAS,WAAW,CAAa,KAAuC;IACvE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,IAAI,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACvI,CAAC;AAED,MAAM,UAAU,MAAM,CACrB,IAA4C;IAE5C,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;QAC1B,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC;QACtB,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { TestContext } from "vitest";
2
+ export interface Refusal {
3
+ readonly cause: unknown;
4
+ readonly index: number;
5
+ }
6
+ export interface Narration {
7
+ at: Error | undefined;
8
+ given: number;
9
+ readonly lines: string[];
10
+ refused: Refusal | undefined;
11
+ readonly sites: Error[];
12
+ readonly tell: (line: string) => void;
13
+ readonly tellAt: (line: string, site: Error) => void;
14
+ }
15
+ export interface Subject<TEngine> {
16
+ readonly engine: TEngine;
17
+ readonly inspect: ((engine: TEngine) => unknown) | undefined;
18
+ readonly name: string;
19
+ }
20
+ export declare function narrate<TEngine>(subject: Subject<TEngine>, { onTestFailed }: Pick<TestContext, "onTestFailed">): Narration;
21
+ export declare function tellGiven(narration: Narration, given: readonly {
22
+ readonly line: string;
23
+ readonly site: Error;
24
+ }[]): void;
25
+ //# sourceMappingURL=narration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narration.d.ts","sourceRoot":"","sources":["../../src/internal/narration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAK1C,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACzB,EAAE,EAAE,KAAK,GAAG,SAAS,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,OAAO,CAAC,OAAO;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,YAAY,EAAE,EAAE,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,SAAS,CAuB1H;AAED,wBAAgB,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,EAAE,GAAG,IAAI,CAKvH"}
@@ -0,0 +1,34 @@
1
+ import { callSite } from "#internal/callSite.ts";
2
+ import { failureReport } from "#internal/failureReport.ts";
3
+ import { describeFailure } from "#internal/render.ts";
4
+ export function narrate(subject, { onTestFailed }) {
5
+ const narration = {
6
+ at: undefined,
7
+ given: 0,
8
+ lines: [],
9
+ refused: undefined,
10
+ sites: [],
11
+ tell: (line) => {
12
+ narration.tellAt(line, callSite());
13
+ },
14
+ tellAt: (line, site) => {
15
+ narration.lines.push(line);
16
+ narration.sites.push(site);
17
+ narration.at = site;
18
+ },
19
+ };
20
+ onTestFailed(({ task }) => {
21
+ const failure = task.result?.errors?.[0];
22
+ if (failure !== undefined) {
23
+ failure.message = `${failure.message === "" ? describeFailure(failure) : failure.message}\n\n${failureReport(narration, subject, { file: task.file.filepath, name: task.name, stack: failure.stack })}`;
24
+ }
25
+ });
26
+ return narration;
27
+ }
28
+ export function tellGiven(narration, given) {
29
+ for (const part of given) {
30
+ narration.tellAt(part.line, part.site);
31
+ }
32
+ narration.given = given.length;
33
+ }
34
+ //# sourceMappingURL=narration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narration.js","sourceRoot":"","sources":["../../src/internal/narration.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAuBtD,MAAM,UAAU,OAAO,CAAU,OAAyB,EAAE,EAAE,YAAY,EAAqC;IAC9G,MAAM,SAAS,GAAc;QAC5B,EAAE,EAAE,SAAS;QACb,KAAK,EAAE,CAAC;QACR,KAAK,EAAE,EAAE;QACT,OAAO,EAAE,SAAS;QAClB,KAAK,EAAE,EAAE;QACT,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;YACd,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACtB,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3B,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3B,SAAS,CAAC,EAAE,GAAG,IAAI,CAAC;QACrB,CAAC;KACD,CAAC;IACF,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,GAAG,GAAG,OAAO,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,OAAO,aAAa,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC;QACzM,CAAC;IACF,CAAC,CAAC,CAAC;IACH,OAAO,SAAS,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,SAAoB,EAAE,KAAiE;IAChH,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IACD,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;AAChC,CAAC"}