@shivaedev/test-story 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 - 2026-10-17
4
+
5
+ ### Fixed
6
+
7
+ - Run engine creation without the story's parent span, so in-process HTTP handlers capture an independent context: requests without trace headers start root traces, and `traceparent` continues the supplied trace. Traits, steps and the story body retain their story span.
8
+
3
9
  ## 0.4.0 - 2026-10-17
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -146,6 +146,63 @@ const app = storyKit({
146
146
 
147
147
  Routing uses the global dispatcher that Node's built-in `fetch` already uses. It is installed before `around`, creation, traits and the body, stays active during scoped cleanup, and restores the previous dispatcher after each story, including a failing story. Stories within one worker must run one at a time because that dispatcher is shared; parallelism belongs between workers or inside one story. The DOM environment uses the same routing for host-side Node fetch and document requests, including redirect destinations. It refuses external hosts even when the kit omits `outbound`.
148
148
 
149
+ #### Asserting on application traces
150
+
151
+ Start an in-process host in `create`. That hook runs without the ambient parent span, so an Effect HTTP handler that captures its context does not inherit a span placed around the story. A request without trace headers starts a root trace; a request with `traceparent` continues that trace. The tracer, services and per-story Scope remain available during creation. Traits, engine steps and the story body keep the span supplied by `around`.
152
+
153
+ A kit can collect spans with a native in-memory tracer, or start a fake OTLP receiver and point the application's exporter at it. Keep collection and any exporter flush or bounded wait in the kit's verbs. Assert only after the request span has ended. This small in-memory kit uses Effect's Web Request handler as its host:
154
+
155
+ ```ts
156
+ import { Effect, Tracer } from "effect";
157
+ import { HttpEffect, HttpServerResponse } from "effect/http";
158
+ import { expect } from "vitest";
159
+ import { storyKit } from "@shivaedev/test-story/storyKit.ts";
160
+
161
+ const spans: Tracer.NativeSpan[] = [];
162
+ const tracer = Tracer.make({
163
+ span: (options) => {
164
+ const span = new Tracer.NativeSpan(options);
165
+ spans.push(span);
166
+ return span;
167
+ },
168
+ });
169
+
170
+ const app = storyKit({
171
+ around: (effect) => effect.pipe(Effect.withSpan("story"), Effect.withTracer(tracer)),
172
+ *create() {
173
+ const context = yield* Effect.context<never>();
174
+ return HttpEffect.toWebHandlerWith(context)(Effect.succeed(HttpServerResponse.text("ok")));
175
+ },
176
+ name: "traced application",
177
+ stages: ["ready"],
178
+ verbs: (handler) => ({
179
+ request: (headers: HeadersInit = {}) => Effect.promise(async () => {
180
+ const response = await handler(new Request("http://app.test/", { headers }));
181
+ await response.text();
182
+ const span = spans.filter((span) => span.kind === "server").at(-1);
183
+ if (span === undefined) throw new Error("The request exported no server span");
184
+ return span;
185
+ }),
186
+ }),
187
+ });
188
+
189
+ app.it("a request is its own root trace", [], function* ({ request }) {
190
+ const storySpan = yield* Effect.currentSpan;
191
+ const span = yield* request();
192
+ expect(span.parent._tag).toBe("None");
193
+ expect(span.traceId).not.toBe(storySpan.traceId);
194
+ expect(span.status._tag).toBe("Ended");
195
+ });
196
+
197
+ app.it("a request continues its supplied trace", [], function* ({ request }) {
198
+ const span = yield* request({ traceparent: "00-1234567890abcdef1234567890abcdef-1234567890abcdef-01" });
199
+ expect(span.traceId).toBe("1234567890abcdef1234567890abcdef");
200
+ expect(span.parent).toMatchObject({ value: { spanId: "1234567890abcdef" } });
201
+ });
202
+ ```
203
+
204
+ The package's [tracing kit](https://github.com/ShivaeDev/platform/blob/main/packages/test-story/src/test-support/tracingApp.ts) and [stories](https://github.com/ShivaeDev/platform/blob/main/packages/test-story/src/server/hostTracing.spec.ts) also assert that two requests have different trace IDs, request work stays under its request span, and traits and engine steps stay under the story span. Creation only removes the incoming parent: spans that application startup explicitly creates still follow native Effect semantics.
205
+
149
206
  ### 2. Once per feature: traits and verbs
150
207
 
151
208
  A trait is a line and a change, tagged with a stage:
@@ -1 +1 @@
1
- {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../../src/server/internal/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAK1C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAQ,MAAM,mBAAmB,CAAC;AAEjG,eAAO,MAAM,KAAK;;oBAehB,CAAC"}
1
+ {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../../src/server/internal/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,MAAM,EAAU,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAK1C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAQ,MAAM,mBAAmB,CAAC;AAEjG,eAAO,MAAM,KAAK;;oBAehB,CAAC"}
@@ -1,11 +1,11 @@
1
- import { Effect } from "effect";
1
+ import { Context, Effect, Tracer } from "effect";
2
2
  import { callSite } from "#internal/callSite.ts";
3
3
  import { hooked } from "#internal/hooked.ts";
4
4
  import { narrate } from "#internal/narration.ts";
5
5
  import { runUntil } from "#internal/runUntil.ts";
6
6
  import { seed } from "#internal/seed.ts";
7
7
  export const start = Effect.fnUntraced(function* (definition, given, context) {
8
- const engine = yield* hooked(definition.create);
8
+ const engine = yield* hooked(definition.create).pipe(Effect.updateContext(Context.omit(Tracer.ParentSpan)));
9
9
  const narration = narrate({ engine, inspect: definition.inspect, name: definition.name }, context);
10
10
  yield* seed(definition, engine, narration, given);
11
11
  const story = {
@@ -1 +1 @@
1
- {"version":3,"file":"start.js","sourceRoot":"","sources":["../../../src/server/internal/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAA0D,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEjG,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,UAA2B,EAAE,KAA4B,EAAE,OAAoB;IAChI,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;IACnG,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAClD,MAAM,KAAK,GAAe;QACzB,MAAM;QACN,KAAK,EAAE,SAAS,CAAC,KAAK;QACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;YAC5B,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;YACxB,SAAS,CAAC,EAAE,GAAG,IAAI,CAAC;YACpB,OAAO,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QACvI,CAAC;QACD,IAAI,EAAE,SAAS,CAAC,IAAI;KACpB,CAAC;IACF,OAAO,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC;AACtD,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"start.js","sourceRoot":"","sources":["../../../src/server/internal/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAA0D,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEjG,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,UAA2B,EAAE,KAA4B,EAAE,OAAoB;IAChI,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC5G,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;IACnG,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAClD,MAAM,KAAK,GAAe;QACzB,MAAM;QACN,KAAK,EAAE,SAAS,CAAC,KAAK;QACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;YAC5B,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;YACxB,SAAS,CAAC,EAAE,GAAG,IAAI,CAAC;YACpB,OAAO,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QACvI,CAAC;QACD,IAAI,EAAE,SAAS,CAAC,IAAI;KACpB,CAAC;IACF,OAAO,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC;AACtD,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/test-story",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Tests that read like stories over the real engine: a story kit per engine with its own it, traits per feature, and a failure that prints the story, where it stopped and the engine's state",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/test-story#readme",
6
6
  "bugs": {
@@ -1,4 +1,4 @@
1
- import { Effect } from "effect";
1
+ import { Context, Effect, Tracer } from "effect";
2
2
  import type { TestContext } from "vitest";
3
3
  import { callSite } from "#internal/callSite.ts";
4
4
  import { hooked } from "#internal/hooked.ts";
@@ -7,7 +7,7 @@ import { runUntil } from "#internal/runUntil.ts";
7
7
  import { type LooseDefinition, type LooseStory, type LooseTrait, seed } from "#internal/seed.ts";
8
8
 
9
9
  export const start = Effect.fnUntraced(function* (definition: LooseDefinition, given: readonly LooseTrait[], context: TestContext) {
10
- const engine = yield* hooked(definition.create);
10
+ const engine = yield* hooked(definition.create).pipe(Effect.updateContext(Context.omit(Tracer.ParentSpan)));
11
11
  const narration = narrate({ engine, inspect: definition.inspect, name: definition.name }, context);
12
12
  yield* seed(definition, engine, narration, given);
13
13
  const story: LooseStory = {