@shivaedev/test-story 0.0.0 → 0.1.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +284 -1
  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 +17 -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 +36 -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 +41 -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 +29 -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 +51 -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 +67 -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 +23 -0
  70. package/package.json +67 -3
  71. package/src/genreTags.ts +10 -0
  72. package/src/internal/callSite.ts +19 -0
  73. package/src/internal/engineSnapshot.ts +48 -0
  74. package/src/internal/errors.ts +39 -0
  75. package/src/internal/failureReport.ts +55 -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 +56 -0
  82. package/src/internal/start.ts +24 -0
  83. package/src/internal/storyIt.ts +68 -0
  84. package/src/internal/traits.ts +20 -0
  85. package/src/internal/wrap.ts +50 -0
  86. package/src/storyKit.ts +130 -0
  87. package/login.log +0 -26
  88. package/publish.log +0 -0
  89. package/run.out +0 -26
  90. package/run.sh +0 -10
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 - 2026-10-12
4
+
5
+ ### Added
6
+
7
+ - Story creation, staged hooks, run hooks, traits and bodies can use the runner’s per-test `Scope` without a worker Layer supplying it. Unrelated services still require a matching Layer.
8
+ - `StoryKitDefinition.around` wraps creation, staged seeding and the body while leaving worker acquisition outside the wrapper.
9
+ - `StoryKitDefinition.clock` selects the kit’s default clock; per-story clock options take precedence.
10
+
11
+ ### Changed
12
+
13
+ - Verify `skip`, `skipIf` and `runIf` in an isolated runner, with passing suite assertions for registration and setup/body non-execution instead of skipped suite tests.
14
+ - Clarify roadmap ownership, failure-narration lifetime and output limits; keep the existing README and `docs/` publication shape.
15
+ - Build package JavaScript, declarations and source maps with TypeScript 7.
16
+
17
+ ## 0.1.0 - 2026-10-06
18
+
19
+ ### Added
20
+
21
+ - `@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.
22
+ - 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.
23
+ - 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.
24
+ - 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.
25
+ - `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.
26
+ - 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,286 @@
1
1
  # @shivaedev/test-story
2
2
 
3
- Placeholder that reserves the name. Story DSL for tests: domain traits, a staged seed, a story log and a settle loop.
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
+ | `around(effect)` | Optional. Wraps creation, staged seeding and the body; worker Layer acquisition remains outside it. It may require the worker services and per-test Scope. |
111
+ | `clock` | Optional. `"test"` by default, or `"live"` for HTTP and DOM stories. A story’s `options.clock` takes precedence. |
112
+ | `create()` | A fresh engine. Every story starts from a new one. |
113
+ | `layer` | Optional. The worker Effect layer that provides the services hooks, traits and test bodies use. Per-test Scope is already available to creation, staged and run hooks, traits and bodies. 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. |
114
+ | `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. |
115
+ | `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. |
116
+ | `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. |
117
+ | `inspect` | Optional. The part of the engine a failure prints. Without it, the failure prints the whole engine. |
118
+ | `verbs(engine, story)` | The entry points a test acts through. A verb that runs the engine returns the Effect from `story.runUntil`. |
119
+
120
+ 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:
121
+
122
+ ```ts
123
+ import { genreTags } from "@shivaedev/test-story/genreTags.ts";
124
+ import { defineConfig } from "vitest/config";
125
+
126
+ export default defineConfig({
127
+ test: { tags: genreTags("bakery", "mill") },
128
+ });
129
+ ```
130
+
131
+ `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.
132
+
133
+ ### 2. Once per feature: traits and verbs
134
+
135
+ A trait is a line and a change, tagged with a stage:
136
+
137
+ ```ts
138
+ export function hasDough(count: number) {
139
+ return bakery.trait("pantry", `the baker has ${count} balls of dough`, (state) => {
140
+ if (count > state.capacity) {
141
+ throw new Error(`the bowls hold only ${state.capacity}`);
142
+ }
143
+ state.dough = count;
144
+ });
145
+ }
146
+
147
+ export function hasFlourDelivered(sacks: number) {
148
+ return bakery.trait("pantry", `the supplier has delivered ${sacks} sacks of flour`, function* (state) {
149
+ const supplier = yield* Supplier;
150
+ yield* supplier.deliver(sacks);
151
+ state.flour = sacks;
152
+ });
153
+ }
154
+
155
+ export function morningShift() {
156
+ return bakery.traits(ovenIsLit(), hasBowls(1));
157
+ }
158
+ ```
159
+
160
+ `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.
161
+
162
+ 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:
163
+
164
+ ```text
165
+ the trait "the baker has 4 balls of dough" refused to set up the bakery: the bowls hold only 3
166
+ help: a trait throws when the bakery it asks for cannot exist. Give the test traits that fit together, or fix the trait
167
+ in the bakery story kit if this bakery should be possible.
168
+
169
+ ╭─ test-story: how to read the story below
170
+ │ "given" lines are the test's traits, the other lines were told by verbs and engine steps as they ran, and ✗ marks
171
+ │ where the test stopped. The traits, verbs and steps live in the bakery story kit that this test imports.
172
+ ╰─
173
+
174
+ given the oven is lit
175
+ ✗ given the baker has 4 balls of dough
176
+ refused at src/test-support/failingStories.ts:13:57
177
+ given the bakery has 1 bowls
178
+
179
+ The bakery when the test failed:
180
+ {"bowls":1,"capacity":3,"dough":0,"flour":0,"loaves":0,"minute":0,"orders":[],"ovenLit":true,"overfired":false,
181
+ "smoking":false,"starter":false}
182
+
183
+ Rerun: vitest run src/test-support/failingStories.ts -t "Bakery Story: marks the trait that refused"
184
+ ```
185
+
186
+ 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.
187
+
188
+ 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.
189
+
190
+ ### 3. In every test: a spec
191
+
192
+ ```ts
193
+ bakery.it("tells each step into the story", [ovenIsLit()], function* ({ baker, oven, story }) {
194
+ baker.kneads(2);
195
+ yield* oven.bakesEverything();
196
+
197
+ expect(story.lines).toEqual([
198
+ "the oven is lit",
199
+ "the baker kneads 2 balls of dough",
200
+ "1m a loaf comes out of the oven",
201
+ "2m a loaf comes out of the oven",
202
+ ]);
203
+ });
204
+ ```
205
+
206
+ `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.
207
+
208
+ 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.
209
+
210
+ ### Running the engine
211
+
212
+ `story.runUntil(condition, { maxSteps })` returns an Effect that asks three questions before every step:
213
+
214
+ 1. `run.failed` returns a sentence: it dies with it, even when the condition also holds.
215
+ 2. The condition holds: it succeeds.
216
+ 3. `maxSteps` steps have run, or the kit's `run.maxSteps` when the call names none: it dies with `run.diagnose`.
217
+
218
+ ```text
219
+ the bakery ran 12 steps and never reached what runUntil waits for: 1 balls of dough still wait after 12 minutes: the
220
+ sourdough starter never runs out
221
+ help: either the bakery never gets there, so check the setup and the engine, or it needs more steps, so pass a larger
222
+ maxSteps to runUntil.
223
+ ```
224
+
225
+ 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.
226
+
227
+ ### When a story fails
228
+
229
+ After the engine has been created, a failing story test adds the story to its first error, as in the examples above:
230
+
231
+ - a short guide to reading the story, in a box;
232
+ - every line in order, traits marked `given`;
233
+ - `✗` 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;
234
+ - 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;
235
+ - the command that reruns the test, with `-t` on a second line when one line would pass 120 columns.
236
+
237
+ The package wraps between words or JSON fields toward 120 columns and prints the story's own lines whole. An indivisible word, field or rerun command component can exceed that width. A passing test adds nothing. A typed failure with no message, such as an `after` hook's `NoBowls`, prints by its tag.
238
+
239
+ ### Whole-stack application kits
240
+
241
+ Keep one kit in the application’s `test-support/`. `effect-test` owns Vitest registration, worker Layer acquisition, the per-test Scope, cancellation and clock selection. `test-story` owns engine creation, staged traits, verbs and narration. The application owns database lifecycle, migrations, isolation, seeds, actual server/client wiring and screen interactions.
242
+
243
+ The [order feature kit](https://github.com/ShivaeDev/platform/tree/main/packages/effect-react/src/test-support/order-example) runs the existing feature’s services, repository, contracts, JSON serialization and authentication middleware over a loopback HTTP listener. Its [stories](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/orderStory.dom.spec.tsx) test visible commits through a separate PostgreSQL pool, rollback of a rejected write without publication, cross-owner rejection, a native React action refreshing a list, and an editor retaining rejected input before a corrected save.
244
+
245
+ ```ts
246
+ import { app, realPostgres } from "./test-support/application.ts";
247
+ import { aliceHasOrder } from "./test-support/traits.ts";
248
+
249
+ app.it("a save commits and a rejected write leaves storage intact", [realPostgres(), aliceHasOrder("paper")], function* ({ saveOrder, rejectOrder, storedOrderIs, changesAre }) {
250
+ yield* saveOrder("paper", "Office chairs", 8);
251
+ yield* storedOrderIs("paper", "Office chairs", 8);
252
+ yield* rejectOrder("paper", -1);
253
+ yield* storedOrderIs("paper", "Office chairs", 8);
254
+ yield* changesAre(1);
255
+ });
256
+ ```
257
+
258
+ The application’s worker Layer leases a uniquely named disposable database, opens native PgClient pools and runs `migratePostgres`. Multiple kits in the worker share the database; the last lease drops it after the pools close. The PostgreSQL trait checks that infrastructure and the declared policy; it does not start a database. The application validates its local disposable destination before database creation or reset and skips database stories without `PLATFORM_EFFECT_SQL_TEST_DATABASE_URL`. Tests sharing that database run serially; parallelism belongs between workers or inside one story.
259
+
260
+ | Application mode | Reset policy | Transaction policy | Coverage |
261
+ | --- | --- | --- | --- |
262
+ | HTTP | Truncate application tables and restart identities before and after the story; retain the migration ledger. | Commands perform real commits. | RPC serialization/transport, independent persistence reads, React refresh and forms. |
263
+ | Local | Reset the baseline before and after the story. | Wrap creation, seeds and body in an outer Platform `transact`, then force rollback after success with a private sentinel. | Local repositories and services, transactional seeds and nested writes. |
264
+
265
+ The kit’s `around` implements that policy. HTTP requests run in separate fibers, so an outer test transaction cannot isolate their writes. Local rollback starts with Platform `transact`; putting it inside a native outer transaction violates transaction ownership. Catch only the wrapper’s private rollback sentinel, let other failures escape, and check independent storage before cleanup truncation so cleanup cannot hide an accidental commit.
266
+
267
+ Application-authored seed builders reuse application schemas, validate overrides, insert actual rows and return generated values. Traits retain aliases such as `"paper"` so specs avoid generated IDs. There is no shared factory or automatic relationship seeding.
268
+
269
+ React screen traits acquire roots in Scope and use a fresh native atom registry for each screen. Cleanup unmounts roots before disposing registries and servers. Screen verbs dispatch DOM events and use bounded observations of rendered output. Select a live clock once on an HTTP/DOM kit; keep TestClock for deterministic local stories. `runUntil` is for engines with a meaningful step hook, not HTTP or DOM polling. Creation forms can use the same pattern with the existing `useCreate` path.
270
+
271
+ The example proves DOM and loopback HTTP behavior, not browser credentials, BetterAuth cookies, SSR, mobile or a deployed proxy. Native `RpcTest` can exercise handlers in faster local stories, but omits serialization and transport. Failure inspection is synchronous: keep redacted observed values in the engine instead of trying to query PostgreSQL inside `inspect`.
272
+
273
+ ### Install and limits
274
+
275
+ ```sh
276
+ pnpm add --save-dev @shivaedev/test-story @effect/vitest@4.0.0-rc.112 effect@4.0.0-rc.112 vitest@4.1.11
277
+ ```
278
+
279
+ `@effect/vitest`, `effect` and `vitest` are peers, so the application's own versions run the stories.
280
+
281
+ - The package runs inside Vitest. The story reaches the failure by rewriting the first error's message in Vitest's `onTestFailed` hook.
282
+ - Layer acquisition and engine creation failures precede that hook and carry no story appendix. A printed rerun command still needs a Vitest configuration that collects its file; the intentional `failingStories.ts` examples use the package's explicit fixture runner.
283
+ - A kit's `it` is created when the kit is defined, so a kit lives in a module that only Vitest test files import.
284
+ - `inspect` runs after the test has failed, so it reads the engine synchronously.
285
+ - 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.
286
+ - 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":"AAMA,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,17 @@
1
+ import { relative } from "node:path";
2
+ import process from "node:process";
3
+ const FRAMES = 50;
4
+ // V8 formats a stack only when it is read, so a site costs little until a test fails.
5
+ export function callSite() {
6
+ const limit = Error.stackTraceLimit;
7
+ Error.stackTraceLimit = FRAMES;
8
+ const site = new Error("call site");
9
+ Error.stackTraceLimit = limit;
10
+ return site;
11
+ }
12
+ export function locate(stack, testFile) {
13
+ const frame = stack?.split("\n").find((line) => line.includes(testFile));
14
+ const position = frame?.slice(frame.indexOf(testFile) + testFile.length).match(/^:(?<line>\d+):(?<column>\d+)/u)?.groups;
15
+ return position === undefined ? undefined : `${relative(process.cwd(), testFile)}:${position.line}:${position.column}`;
16
+ }
17
+ //# 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;AACrC,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,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":"AAGA,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,36 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import process from "node:process";
4
+ import { describeValue, toJson } from "#internal/render.ts";
5
+ import { WIDTH, wrap, wrapJson } from "#internal/wrap.ts";
6
+ const INLINE_CHARACTERS = 2000;
7
+ function snapshot(subject) {
8
+ try {
9
+ return { value: subject.inspect === undefined ? subject.engine : subject.inspect(subject.engine) };
10
+ }
11
+ catch (threw) {
12
+ return { threw };
13
+ }
14
+ }
15
+ function slug(text) {
16
+ return text
17
+ .toLowerCase()
18
+ .replaceAll(/[^a-z0-9.]+/gu, "-")
19
+ .slice(0, 80);
20
+ }
21
+ export function engineSnapshot(subject, test) {
22
+ const heading = `The ${subject.name} when the test failed`;
23
+ const taken = snapshot(subject);
24
+ if ("threw" in taken) {
25
+ return wrap(`${heading} is unknown: its inspect hook threw ${describeValue(taken.threw)}`);
26
+ }
27
+ const json = toJson(taken.value);
28
+ if (json.length <= INLINE_CHARACTERS) {
29
+ return heading.length + 2 + json.length > WIDTH ? [`${heading}:`, ...wrapJson(json)] : [`${heading}: ${json}`];
30
+ }
31
+ const file = join(process.cwd(), "node_modules", ".cache", "test-story", `${slug(basename(test.file))}--${slug(test.name)}--${slug(subject.name)}.json`);
32
+ mkdirSync(dirname(file), { recursive: true });
33
+ writeFileSync(file, toJson(taken.value, 2));
34
+ return wrap(`${heading} is ${json.length} characters of JSON, too long to print here. Read it in ${relative(process.cwd(), file)}`);
35
+ }
36
+ //# 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;AAC9D,OAAO,OAAO,MAAM,cAAc,CAAC;AAGnC,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":"AAIA,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,41 @@
1
+ import { relative } from "node:path";
2
+ import process from "node:process";
3
+ import { locate } from "#internal/callSite.ts";
4
+ import { engineSnapshot } from "#internal/engineSnapshot.ts";
5
+ import { describeValue } from "#internal/render.ts";
6
+ import { WIDTH, wrap } from "#internal/wrap.ts";
7
+ export function failureReport(narration, subject, test) {
8
+ try {
9
+ return [...guide(subject.name), "", ...storyRows(narration, test), "", ...engineSnapshot(subject, test), "", rerun(test)].join("\n");
10
+ }
11
+ catch (error) {
12
+ return `test-story could not print the story of this ${subject.name}: ${describeValue(error)}`;
13
+ }
14
+ }
15
+ function guide(name) {
16
+ return [
17
+ "╭─ test-story: how to read the story below",
18
+ ...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: "│ " }),
19
+ "╰─",
20
+ ];
21
+ }
22
+ function storyRows(narration, test) {
23
+ const rows = narration.lines.map((line, index) => `${narration.refused?.index === index ? "✗" : " "} ${index < narration.given ? "given" : " "} ${line}`);
24
+ if (narration.refused !== undefined) {
25
+ const where = locate(narration.sites[narration.refused.index]?.stack, test.file);
26
+ rows.splice(narration.refused.index + 1, 0, ...(where === undefined ? [] : [` refused at ${where}`]));
27
+ return rows;
28
+ }
29
+ const where = locate(test.stack, test.file) ?? locate(narration.at?.stack, test.file);
30
+ return [
31
+ ...rows,
32
+ `✗ the test failed ${rows.length === 0 ? "before the story told a line" : "after the line above"}`,
33
+ ...(where === undefined ? [] : [` at ${where}`]),
34
+ ];
35
+ }
36
+ function rerun(test) {
37
+ const run = `Rerun: vitest run ${relative(process.cwd(), test.file)}`;
38
+ const filter = `-t "${test.name.replaceAll(/[$()*+.?[\\\]^{|}]/gu, "\\$&")}"`;
39
+ return run.length + 1 + filter.length > WIDTH ? `${run} \\\n ${filter}` : `${run} ${filter}`;
40
+ }
41
+ //# sourceMappingURL=failureReport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failureReport.js","sourceRoot":"","sources":["../../src/internal/failureReport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAQhD,MAAM,UAAU,aAAa,CAAU,SAAoB,EAAE,OAAyB,EAAE,IAAgB;IACvG,IAAI,CAAC;QACJ,OAAO,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtI,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,gDAAgD,OAAO,CAAC,IAAI,KAAK,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;IAChG,CAAC;AACF,CAAC;AAED,SAAS,KAAK,CAAC,IAAY;IAC1B,OAAO;QACN,4CAA4C;QAC5C,GAAG,IAAI,CACN,qLAAqL,IAAI,oCAAoC,EAC7N,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAC3B;QACD,IAAI;KACJ,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,SAAoB,EAAE,IAAgB;IACxD,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAC/B,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC,OAAO,EAAE,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,EAAE,CAC5H,CAAC;IACF,IAAI,SAAS,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACjF,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,uBAAuB,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9G,OAAO,IAAI,CAAC;IACb,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACtF,OAAO;QACN,GAAG,IAAI;QACP,4BAA4B,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,sBAAsB,EAAE;QACzG,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,eAAe,KAAK,EAAE,CAAC,CAAC;KACxD,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,IAAgB;IAC9B,MAAM,GAAG,GAAG,qBAAqB,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACtE,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,sBAAsB,EAAE,MAAM,CAAC,GAAG,CAAC;IAC9E,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,UAAU,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,MAAM,EAAE,CAAC;AAC/F,CAAC"}
@@ -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"}