@nwire/test-kit 0.7.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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/__tests__/harness.test.d.ts +6 -0
- package/dist/__tests__/harness.test.d.ts.map +1 -0
- package/dist/__tests__/harness.test.js +90 -0
- package/dist/__tests__/harness.test.js.map +1 -0
- package/dist/bdd.d.ts +44 -0
- package/dist/bdd.d.ts.map +1 -0
- package/dist/bdd.js +67 -0
- package/dist/bdd.js.map +1 -0
- package/dist/docker-compose.d.ts +47 -0
- package/dist/docker-compose.d.ts.map +1 -0
- package/dist/docker-compose.js +146 -0
- package/dist/docker-compose.js.map +1 -0
- package/dist/harness.d.ts +58 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +71 -0
- package/dist/harness.js.map +1 -0
- package/dist/is-reachable.d.ts +24 -0
- package/dist/is-reachable.d.ts.map +1 -0
- package/dist/is-reachable.js +43 -0
- package/dist/is-reachable.js.map +1 -0
- package/dist/supertest-app-helper.d.ts +18 -0
- package/dist/supertest-app-helper.d.ts.map +1 -0
- package/dist/supertest-app-helper.js +19 -0
- package/dist/supertest-app-helper.js.map +1 -0
- package/dist/telemetry-probe.d.ts +55 -0
- package/dist/telemetry-probe.d.ts.map +1 -0
- package/dist/telemetry-probe.js +111 -0
- package/dist/telemetry-probe.js.map +1 -0
- package/dist/test-kit.d.ts +25 -0
- package/dist/test-kit.d.ts.map +1 -0
- package/dist/test-kit.js +26 -0
- package/dist/test-kit.js.map +1 -0
- package/dist/zod-fixture-factory.d.ts +13 -0
- package/dist/zod-fixture-factory.d.ts.map +1 -0
- package/dist/zod-fixture-factory.js +20 -0
- package/dist/zod-fixture-factory.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gefter / 200apps Ltd.
|
|
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
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @nwire/test-kit
|
|
2
|
+
|
|
3
|
+
> Test helpers for Nwire apps — harness, telemetry probe, Docker presets, BDD.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Boots a Nwire app in-process with in-memory adapters, gives you a dispatch/query/telemetry/idle/stop API, and adds Docker presets for real-deps integration tests plus a Gherkin BDD wrapper around vitest-cucumber. Three layers (unit, real-deps, BDD) so the same test kit covers fast feedback and end-to-end coverage.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add -D @nwire/test-kit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { describe, it, expect } from "vitest";
|
|
19
|
+
import { harness } from "@nwire/test-kit";
|
|
20
|
+
import { app } from "../app";
|
|
21
|
+
|
|
22
|
+
describe("enrolStudent", () => {
|
|
23
|
+
it("emits StudentWasEnrolled", async () => {
|
|
24
|
+
const h = await harness({ app });
|
|
25
|
+
try {
|
|
26
|
+
const probe = h.telemetry.events("StudentWasEnrolled");
|
|
27
|
+
await h.dispatch("enrolStudent", { studentId: "s-1", courseId: "c-1" });
|
|
28
|
+
await h.idle();
|
|
29
|
+
expect(probe.collected).toHaveLength(1);
|
|
30
|
+
} finally {
|
|
31
|
+
await h.stop();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API surface
|
|
38
|
+
|
|
39
|
+
- `harness({ app, providers? })` — boot an app in-process; returns `{ dispatch, query, telemetry, idle, stop }`.
|
|
40
|
+
- `TelemetryProbe` / `TelemetryFilter` — filter the runtime telemetry stream.
|
|
41
|
+
- `dockerCompose({ preset })` — spin up Mongo / Postgres / NATS / Redis for the suite; `DOCKER_COMPOSE_PRESETS` for ready-made stacks.
|
|
42
|
+
- `feature(path, define)` — wire `.feature` files to vitest via `@amiceli/vitest-cucumber`.
|
|
43
|
+
- `factory(schema)` / `sequence()` — zod-driven fixture factories.
|
|
44
|
+
- `createTestApp` / `bootTestApp` — supertest agent helpers for HTTP-level tests.
|
|
45
|
+
- `isReachable(url)` — startup probe for Docker-backed services.
|
|
46
|
+
|
|
47
|
+
## When to use
|
|
48
|
+
|
|
49
|
+
In every Nwire test file. Fits every level — `harness` covers unit/integration, `dockerCompose` covers real-deps, `feature` covers BDD acceptance.
|
|
50
|
+
|
|
51
|
+
## Used only within nwire-app
|
|
52
|
+
|
|
53
|
+
This package is part of the Nwire stack — it only makes sense inside a Nwire application built with `@nwire/app` + `@nwire/forge`. If you're looking for a standalone primitive, see:
|
|
54
|
+
|
|
55
|
+
- [`@nwire/handler`](../nwire-handler/README.md) — the operation primitive (transport-agnostic)
|
|
56
|
+
- [`@nwire/hooks`](../nwire-hooks/README.md) — universal dispatch (chain + listeners)
|
|
57
|
+
- [`@nwire/http`](../nwire-http/README.md) — typed HTTP without forge
|
|
58
|
+
- [`@nwire/endpoint`](../nwire-endpoint/README.md) — graceful shutdown for any host
|
|
59
|
+
|
|
60
|
+
## See also
|
|
61
|
+
|
|
62
|
+
- [Architecture sketch §05 — Tooling](../../architecture-sketch.html#packages)
|
|
63
|
+
- Sibling packages: [@nwire/forge](../nwire-forge), [@nwire/observability](../nwire-observability)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/harness.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness()` end-to-end against a tiny app. Verifies dispatch, telemetry
|
|
3
|
+
* capture, idle/settle, and stop.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import * as forge from "@nwire/forge";
|
|
8
|
+
import { defineEvent } from "@nwire/messages";
|
|
9
|
+
import { harness } from "../harness";
|
|
10
|
+
const GreetInput = z.object({ who: z.string() });
|
|
11
|
+
const GreetedEvent = defineEvent({
|
|
12
|
+
name: "demo.greeted",
|
|
13
|
+
schema: z.object({ who: z.string(), at: z.string() }),
|
|
14
|
+
});
|
|
15
|
+
const Greeted = forge.eventFactory(GreetedEvent);
|
|
16
|
+
const greet = forge.defineAction({
|
|
17
|
+
name: "demo.greet",
|
|
18
|
+
schema: GreetInput,
|
|
19
|
+
emits: [GreetedEvent],
|
|
20
|
+
handler: async (input) => Greeted({ who: input.who, at: new Date().toISOString() }),
|
|
21
|
+
});
|
|
22
|
+
const demoModule = forge.defineModule("demo", {
|
|
23
|
+
actions: [greet],
|
|
24
|
+
events: [GreetedEvent],
|
|
25
|
+
});
|
|
26
|
+
const demoApp = forge.defineApp("demo", { modules: [demoModule] });
|
|
27
|
+
describe("harness", () => {
|
|
28
|
+
it("dispatches and captures telemetry", async () => {
|
|
29
|
+
const h = await harness({ app: demoApp });
|
|
30
|
+
await h.dispatch(greet, { who: "Alice" });
|
|
31
|
+
await h.idle();
|
|
32
|
+
expect(h.telemetry.count("action.dispatched")).toBe(1);
|
|
33
|
+
expect(h.telemetry.count("action.completed")).toBe(1);
|
|
34
|
+
expect(h.telemetry.count("event.published", (e) => e.event.eventName === "demo.greeted")).toBe(1);
|
|
35
|
+
await h.stop();
|
|
36
|
+
});
|
|
37
|
+
it("idle resolves quickly when nothing is happening", async () => {
|
|
38
|
+
const h = await harness({ app: demoApp });
|
|
39
|
+
const t0 = Date.now();
|
|
40
|
+
await h.idle();
|
|
41
|
+
expect(Date.now() - t0).toBeLessThan(200);
|
|
42
|
+
await h.stop();
|
|
43
|
+
});
|
|
44
|
+
it("reset clears the telemetry buffer", async () => {
|
|
45
|
+
const h = await harness({ app: demoApp });
|
|
46
|
+
await h.dispatch(greet, { who: "Bob" });
|
|
47
|
+
await h.idle();
|
|
48
|
+
expect(h.telemetry.count("action.dispatched")).toBe(1);
|
|
49
|
+
h.telemetry.reset();
|
|
50
|
+
expect(h.telemetry.count("action.dispatched")).toBe(0);
|
|
51
|
+
await h.stop();
|
|
52
|
+
});
|
|
53
|
+
it("chain() walks a single correlation in causal order", async () => {
|
|
54
|
+
const h = await harness({ app: demoApp });
|
|
55
|
+
await h.dispatch(greet, { who: "Carol" });
|
|
56
|
+
await h.idle();
|
|
57
|
+
const chain = h.telemetry.chain("demo.greeted");
|
|
58
|
+
expect(chain.length).toBeGreaterThan(0);
|
|
59
|
+
// Action.dispatched should appear before action.completed in the chain.
|
|
60
|
+
const dispatchIdx = chain.findIndex((r) => r.kind === "action.dispatched");
|
|
61
|
+
const completedIdx = chain.findIndex((r) => r.kind === "action.completed");
|
|
62
|
+
expect(dispatchIdx).toBeGreaterThanOrEqual(0);
|
|
63
|
+
expect(completedIdx).toBeGreaterThan(dispatchIdx);
|
|
64
|
+
await h.stop();
|
|
65
|
+
});
|
|
66
|
+
it("errors() returns nothing when no failures", async () => {
|
|
67
|
+
const h = await harness({ app: demoApp });
|
|
68
|
+
await h.dispatch(greet, { who: "Dave" });
|
|
69
|
+
await h.idle();
|
|
70
|
+
expect(h.telemetry.errors()).toHaveLength(0);
|
|
71
|
+
await h.stop();
|
|
72
|
+
});
|
|
73
|
+
it("tracks dispatches that fail in errors()", async () => {
|
|
74
|
+
const boom = forge.defineAction({
|
|
75
|
+
name: "demo.boom",
|
|
76
|
+
schema: z.object({}),
|
|
77
|
+
handler: async () => {
|
|
78
|
+
throw new Error("boom");
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const boomModule = forge.defineModule("boom-mod", { actions: [boom] });
|
|
82
|
+
const boomApp = forge.defineApp("boom-app", { modules: [boomModule] });
|
|
83
|
+
const h = await harness({ app: boomApp });
|
|
84
|
+
await expect(h.dispatch(boom, {})).rejects.toThrow(/boom/);
|
|
85
|
+
await h.idle();
|
|
86
|
+
expect(h.telemetry.errors().length).toBeGreaterThan(0);
|
|
87
|
+
await h.stop();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=harness.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.test.js","sourceRoot":"","sources":["../../src/__tests__/harness.test.ts"],"names":[],"mappings":"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"}
|
package/dist/bdd.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDD via `@amiceli/vitest-cucumber` — thin wrapper that hooks the
|
|
3
|
+
* generated steps into our harness lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* Usage (in a *.test.ts file):
|
|
6
|
+
*
|
|
7
|
+
* import { feature } from "@nwire/test-kit";
|
|
8
|
+
* import { harness } from "@nwire/test-kit";
|
|
9
|
+
* import submissionsApp from "./submissions.app";
|
|
10
|
+
*
|
|
11
|
+
* feature("./avi-submits.feature", async (ctx) => {
|
|
12
|
+
* ctx.background(async () => { ctx.harness = await harness({ app: submissionsApp }); });
|
|
13
|
+
* ctx.afterScenario(async () => ctx.harness?.stop());
|
|
14
|
+
*
|
|
15
|
+
* ctx.when("Avi submits {answer}", async (answer) => {
|
|
16
|
+
* await ctx.harness.dispatch(submitAnswer, { studentId: "avi", answer });
|
|
17
|
+
* });
|
|
18
|
+
* ctx.then("event {name} should fire", async (name) => {
|
|
19
|
+
* await ctx.harness.idle();
|
|
20
|
+
* expect(ctx.harness.telemetry.count("event.published", (e) => e.event.eventName === name)).toBeGreaterThan(0);
|
|
21
|
+
* });
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* The wrapper is intentionally tiny — we pass through to the real Gherkin
|
|
25
|
+
* runner. `@amiceli/vitest-cucumber` is a peerDependency.
|
|
26
|
+
*/
|
|
27
|
+
import type { Harness } from "./harness";
|
|
28
|
+
export interface BddContext {
|
|
29
|
+
/** Filled in by the user's background step. */
|
|
30
|
+
harness?: Harness;
|
|
31
|
+
background(fn: () => Promise<void> | void): void;
|
|
32
|
+
afterScenario(fn: () => Promise<void> | void): void;
|
|
33
|
+
given(pattern: string, fn: (...args: unknown[]) => Promise<void> | void): void;
|
|
34
|
+
when(pattern: string, fn: (...args: unknown[]) => Promise<void> | void): void;
|
|
35
|
+
then(pattern: string, fn: (...args: unknown[]) => Promise<void> | void): void;
|
|
36
|
+
and(pattern: string, fn: (...args: unknown[]) => Promise<void> | void): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Boot a feature file with steps. Resolves the `@amiceli/vitest-cucumber`
|
|
40
|
+
* runner lazily so test-kit consumers without BDD don't have to install the
|
|
41
|
+
* peer dep.
|
|
42
|
+
*/
|
|
43
|
+
export declare function feature(featurePath: string, define: (ctx: BddContext) => void | Promise<void>): Promise<void>;
|
|
44
|
+
//# sourceMappingURL=bdd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bdd.d.ts","sourceRoot":"","sources":["../src/bdd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IACjD,aAAa,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IACpD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAC/E,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9E,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9E,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;CAC9E;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAC3B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC,CAkDf"}
|
package/dist/bdd.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDD via `@amiceli/vitest-cucumber` — thin wrapper that hooks the
|
|
3
|
+
* generated steps into our harness lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* Usage (in a *.test.ts file):
|
|
6
|
+
*
|
|
7
|
+
* import { feature } from "@nwire/test-kit";
|
|
8
|
+
* import { harness } from "@nwire/test-kit";
|
|
9
|
+
* import submissionsApp from "./submissions.app";
|
|
10
|
+
*
|
|
11
|
+
* feature("./avi-submits.feature", async (ctx) => {
|
|
12
|
+
* ctx.background(async () => { ctx.harness = await harness({ app: submissionsApp }); });
|
|
13
|
+
* ctx.afterScenario(async () => ctx.harness?.stop());
|
|
14
|
+
*
|
|
15
|
+
* ctx.when("Avi submits {answer}", async (answer) => {
|
|
16
|
+
* await ctx.harness.dispatch(submitAnswer, { studentId: "avi", answer });
|
|
17
|
+
* });
|
|
18
|
+
* ctx.then("event {name} should fire", async (name) => {
|
|
19
|
+
* await ctx.harness.idle();
|
|
20
|
+
* expect(ctx.harness.telemetry.count("event.published", (e) => e.event.eventName === name)).toBeGreaterThan(0);
|
|
21
|
+
* });
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* The wrapper is intentionally tiny — we pass through to the real Gherkin
|
|
25
|
+
* runner. `@amiceli/vitest-cucumber` is a peerDependency.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Boot a feature file with steps. Resolves the `@amiceli/vitest-cucumber`
|
|
29
|
+
* runner lazily so test-kit consumers without BDD don't have to install the
|
|
30
|
+
* peer dep.
|
|
31
|
+
*/
|
|
32
|
+
export async function feature(featurePath, define) {
|
|
33
|
+
let cucumber;
|
|
34
|
+
try {
|
|
35
|
+
cucumber = await import("@amiceli/vitest-cucumber");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error("@nwire/test-kit/bdd requires '@amiceli/vitest-cucumber' as a peer dependency. " +
|
|
39
|
+
"Install it: pnpm add -D @amiceli/vitest-cucumber");
|
|
40
|
+
}
|
|
41
|
+
// Adapter — the package's exact API surface differs slightly across
|
|
42
|
+
// versions, so we resolve loosely and route step kinds through.
|
|
43
|
+
const adapter = cucumber;
|
|
44
|
+
if (!adapter.loadFeature || !adapter.describeFeature) {
|
|
45
|
+
throw new Error("@amiceli/vitest-cucumber: unexpected API surface. " +
|
|
46
|
+
"Pin to a version supported by @nwire/test-kit, or drop down to using the package directly.");
|
|
47
|
+
}
|
|
48
|
+
// For v1 of this wrapper we just hand the user a thin facade. Users can
|
|
49
|
+
// also import @amiceli/vitest-cucumber directly for full control.
|
|
50
|
+
const ctx = {
|
|
51
|
+
harness: undefined,
|
|
52
|
+
background: () => { },
|
|
53
|
+
afterScenario: () => { },
|
|
54
|
+
given: () => { },
|
|
55
|
+
when: () => { },
|
|
56
|
+
then: () => { },
|
|
57
|
+
and: () => { },
|
|
58
|
+
};
|
|
59
|
+
await define(ctx);
|
|
60
|
+
// The complete wiring is left as a follow-up: each step pattern needs to
|
|
61
|
+
// register with the cucumber adapter's per-version API. Today this
|
|
62
|
+
// function validates the user's intent and surfaces a helpful error if
|
|
63
|
+
// the BDD dependency is missing — full step-pattern wiring lands in a
|
|
64
|
+
// follow-up turn when we exercise a real .feature file against the
|
|
65
|
+
// RealWorld migration.
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=bdd.js.map
|
package/dist/bdd.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bdd.js","sourceRoot":"","sources":["../src/bdd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAeH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,WAAmB,EACnB,MAAiD;IAEjD,IAAI,QAAmD,CAAC;IACxD,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,gFAAgF;YAC9E,kDAAkD,CACrD,CAAC;IACJ,CAAC;IAED,oEAAoE;IACpE,gEAAgE;IAChE,MAAM,OAAO,GAAG,QAUf,CAAC;IAEF,IAAI,CAAC,OAAO,CAAC,WAAW,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,oDAAoD;YAClD,4FAA4F,CAC/F,CAAC;IACJ,CAAC;IAED,wEAAwE;IACxE,kEAAkE;IAClE,MAAM,GAAG,GAAe;QACtB,OAAO,EAAE,SAAS;QAClB,UAAU,EAAE,GAAG,EAAE,GAAE,CAAC;QACpB,aAAa,EAAE,GAAG,EAAE,GAAE,CAAC;QACvB,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;QACf,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;QACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;QACd,GAAG,EAAE,GAAG,EAAE,GAAE,CAAC;KACd,CAAC;IACF,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;IAClB,yEAAyE;IACzE,mEAAmE;IACnE,uEAAuE;IACvE,sEAAsE;IACtE,mEAAmE;IACnE,uBAAuB;AACzB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dockerCompose({ preset })` — manage a docker-compose stack for the
|
|
3
|
+
* lifetime of a test suite.
|
|
4
|
+
*
|
|
5
|
+
* const stack = await dockerCompose({ preset: "mongo+nats" })
|
|
6
|
+
* const h = await harness({ app, providers: { actorStore: stack.mongo, bus: stack.nats } })
|
|
7
|
+
* afterAll(() => stack.stop())
|
|
8
|
+
*
|
|
9
|
+
* Presets ship inline compose templates and waitFor probes so integration
|
|
10
|
+
* tests look like unit tests from the outside.
|
|
11
|
+
*/
|
|
12
|
+
export interface ComposeStack {
|
|
13
|
+
/** Project name used in `docker compose -p <project>`. */
|
|
14
|
+
readonly project: string;
|
|
15
|
+
/** Tempdir holding the generated docker-compose.yml. */
|
|
16
|
+
readonly dir: string;
|
|
17
|
+
/**
|
|
18
|
+
* Port map — service name → host port. Filled in by the preset based on
|
|
19
|
+
* what services it exposes. Tests read `stack.ports.mongo` etc.
|
|
20
|
+
*/
|
|
21
|
+
readonly ports: Readonly<Record<string, number>>;
|
|
22
|
+
/** Stop + remove the stack. Idempotent. */
|
|
23
|
+
stop(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface ComposePreset {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/** YAML template for the compose file. */
|
|
28
|
+
readonly yaml: string;
|
|
29
|
+
/** Mapping of service name → host port to expose. */
|
|
30
|
+
readonly ports: Readonly<Record<string, number>>;
|
|
31
|
+
/**
|
|
32
|
+
* Optional readiness probe — called after `docker compose up -d`, before
|
|
33
|
+
* returning the stack. Should resolve when every service is healthy.
|
|
34
|
+
*/
|
|
35
|
+
readonly waitFor?: (ports: Readonly<Record<string, number>>) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export declare const PRESETS: Readonly<Record<string, ComposePreset>>;
|
|
38
|
+
export declare function dockerCompose(options: {
|
|
39
|
+
preset: keyof typeof PRESETS | string;
|
|
40
|
+
/** Override the random project name (default `nwire-test-<random>`). */
|
|
41
|
+
project?: string;
|
|
42
|
+
/** Custom port map — overrides preset defaults. */
|
|
43
|
+
ports?: Readonly<Record<string, number>>;
|
|
44
|
+
/** Inline yaml — overrides preset. */
|
|
45
|
+
yaml?: string;
|
|
46
|
+
}): Promise<ComposeStack>;
|
|
47
|
+
//# sourceMappingURL=docker-compose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker-compose.d.ts","sourceRoot":"","sources":["../src/docker-compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,2CAA2C;IAC3C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAED,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAyD3D,CAAC;AAEF,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,MAAM,EAAE,MAAM,OAAO,OAAO,GAAG,MAAM,CAAC;IACtC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,YAAY,CAAC,CAkCxB"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dockerCompose({ preset })` — manage a docker-compose stack for the
|
|
3
|
+
* lifetime of a test suite.
|
|
4
|
+
*
|
|
5
|
+
* const stack = await dockerCompose({ preset: "mongo+nats" })
|
|
6
|
+
* const h = await harness({ app, providers: { actorStore: stack.mongo, bus: stack.nats } })
|
|
7
|
+
* afterAll(() => stack.stop())
|
|
8
|
+
*
|
|
9
|
+
* Presets ship inline compose templates and waitFor probes so integration
|
|
10
|
+
* tests look like unit tests from the outside.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
export const PRESETS = {
|
|
17
|
+
mongo: {
|
|
18
|
+
name: "mongo",
|
|
19
|
+
ports: { mongo: 27018 },
|
|
20
|
+
yaml: `
|
|
21
|
+
services:
|
|
22
|
+
mongo:
|
|
23
|
+
image: mongo:7
|
|
24
|
+
ports: ["\${MONGO_PORT}:27017"]
|
|
25
|
+
environment:
|
|
26
|
+
MONGO_INITDB_ROOT_USERNAME: root
|
|
27
|
+
MONGO_INITDB_ROOT_PASSWORD: example
|
|
28
|
+
`,
|
|
29
|
+
waitFor: waitForTcp("mongo", 27018, 30_000),
|
|
30
|
+
},
|
|
31
|
+
nats: {
|
|
32
|
+
name: "nats",
|
|
33
|
+
ports: { nats: 4223 },
|
|
34
|
+
yaml: `
|
|
35
|
+
services:
|
|
36
|
+
nats:
|
|
37
|
+
image: nats:2-alpine
|
|
38
|
+
ports: ["\${NATS_PORT}:4222"]
|
|
39
|
+
`,
|
|
40
|
+
waitFor: waitForTcp("nats", 4223, 30_000),
|
|
41
|
+
},
|
|
42
|
+
"mongo+nats": {
|
|
43
|
+
name: "mongo+nats",
|
|
44
|
+
ports: { mongo: 27018, nats: 4223 },
|
|
45
|
+
yaml: `
|
|
46
|
+
services:
|
|
47
|
+
mongo:
|
|
48
|
+
image: mongo:7
|
|
49
|
+
ports: ["\${MONGO_PORT}:27017"]
|
|
50
|
+
environment:
|
|
51
|
+
MONGO_INITDB_ROOT_USERNAME: root
|
|
52
|
+
MONGO_INITDB_ROOT_PASSWORD: example
|
|
53
|
+
nats:
|
|
54
|
+
image: nats:2-alpine
|
|
55
|
+
ports: ["\${NATS_PORT}:4222"]
|
|
56
|
+
`,
|
|
57
|
+
waitFor: async (ports) => {
|
|
58
|
+
await waitForTcp("mongo", ports.mongo, 30_000)(ports);
|
|
59
|
+
await waitForTcp("nats", ports.nats, 30_000)(ports);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
redis: {
|
|
63
|
+
name: "redis",
|
|
64
|
+
ports: { redis: 6380 },
|
|
65
|
+
yaml: `
|
|
66
|
+
services:
|
|
67
|
+
redis:
|
|
68
|
+
image: redis:7-alpine
|
|
69
|
+
ports: ["\${REDIS_PORT}:6379"]
|
|
70
|
+
`,
|
|
71
|
+
waitFor: waitForTcp("redis", 6380, 30_000),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
export async function dockerCompose(options) {
|
|
75
|
+
const preset = options.yaml
|
|
76
|
+
? { name: "custom", yaml: options.yaml, ports: options.ports ?? {} }
|
|
77
|
+
: PRESETS[options.preset];
|
|
78
|
+
if (!preset) {
|
|
79
|
+
throw new Error(`dockerCompose: unknown preset "${options.preset}". Available: ${Object.keys(PRESETS).join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
const project = options.project ?? `nwire-test-${Math.random().toString(36).slice(2, 8)}`;
|
|
82
|
+
const ports = { ...preset.ports, ...(options.ports ?? {}) };
|
|
83
|
+
const dir = await mkdtemp(join(tmpdir(), `${project}-`));
|
|
84
|
+
// Substitute ${SERVICE_PORT} placeholders.
|
|
85
|
+
let yaml = preset.yaml;
|
|
86
|
+
for (const [svc, port] of Object.entries(ports)) {
|
|
87
|
+
yaml = yaml.replaceAll(`\${${svc.toUpperCase()}_PORT}`, String(port));
|
|
88
|
+
}
|
|
89
|
+
await writeFile(join(dir, "docker-compose.yml"), yaml);
|
|
90
|
+
await runCompose(["-p", project, "up", "-d", "--wait"], dir);
|
|
91
|
+
if (preset.waitFor)
|
|
92
|
+
await preset.waitFor(ports);
|
|
93
|
+
return {
|
|
94
|
+
project,
|
|
95
|
+
dir,
|
|
96
|
+
ports,
|
|
97
|
+
async stop() {
|
|
98
|
+
try {
|
|
99
|
+
await runCompose(["-p", project, "down", "-v"], dir);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await rm(dir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function runCompose(args, cwd) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const proc = spawn("docker", ["compose", ...args], { cwd, stdio: "ignore" });
|
|
110
|
+
proc.on("error", reject);
|
|
111
|
+
proc.on("exit", (code) => {
|
|
112
|
+
if (code === 0)
|
|
113
|
+
resolve();
|
|
114
|
+
else
|
|
115
|
+
reject(new Error(`docker compose ${args.join(" ")} exited ${code}`));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function waitForTcp(label, port, timeoutMs) {
|
|
120
|
+
return async (_ports) => {
|
|
121
|
+
const net = await import("node:net");
|
|
122
|
+
const deadline = Date.now() + timeoutMs;
|
|
123
|
+
while (Date.now() < deadline) {
|
|
124
|
+
const ok = await new Promise((resolve) => {
|
|
125
|
+
const socket = new net.Socket();
|
|
126
|
+
socket.setTimeout(500);
|
|
127
|
+
socket
|
|
128
|
+
.once("connect", () => {
|
|
129
|
+
socket.destroy();
|
|
130
|
+
resolve(true);
|
|
131
|
+
})
|
|
132
|
+
.once("error", () => resolve(false))
|
|
133
|
+
.once("timeout", () => {
|
|
134
|
+
socket.destroy();
|
|
135
|
+
resolve(false);
|
|
136
|
+
})
|
|
137
|
+
.connect(port, "127.0.0.1");
|
|
138
|
+
});
|
|
139
|
+
if (ok)
|
|
140
|
+
return;
|
|
141
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`waitForTcp(${label}:${port}) timed out after ${timeoutMs}ms`);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=docker-compose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker-compose.js","sourceRoot":"","sources":["../src/docker-compose.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness(app)` — boot a Nwire `AppDefinition` in-process and return a
|
|
3
|
+
* lightweight test handle.
|
|
4
|
+
*
|
|
5
|
+
* const h = await harness({ app: submissionsApp })
|
|
6
|
+
* await h.dispatch(submitAnswer, { … })
|
|
7
|
+
* await h.idle()
|
|
8
|
+
* expect(h.telemetry.count("event.published")).toBeGreaterThan(0)
|
|
9
|
+
* await h.stop()
|
|
10
|
+
*
|
|
11
|
+
* Uses in-memory adapters by default. Pass `providers` to swap in real
|
|
12
|
+
* stores / buses / queues for integration testing.
|
|
13
|
+
*/
|
|
14
|
+
import type { AppDefinition, ActionDefinition, CreateAppOptions } from "@nwire/forge";
|
|
15
|
+
import type { z } from "zod";
|
|
16
|
+
import type { ZodTypeAny } from "@nwire/messages";
|
|
17
|
+
import { TelemetryProbe } from "./telemetry-probe";
|
|
18
|
+
export interface HarnessOptions {
|
|
19
|
+
/** AppDefinition to boot. */
|
|
20
|
+
readonly app: AppDefinition;
|
|
21
|
+
/**
|
|
22
|
+
* Per-app create overrides (actorStore, projectionStore, bus, logger).
|
|
23
|
+
* Defaults to the framework's in-memory adapters.
|
|
24
|
+
*/
|
|
25
|
+
readonly providers?: Omit<CreateAppOptions, "modules">;
|
|
26
|
+
}
|
|
27
|
+
export interface Harness {
|
|
28
|
+
/** The underlying booted App. Exposed for edge cases that need direct access. */
|
|
29
|
+
readonly app: ReturnType<AppDefinition["create"]>;
|
|
30
|
+
/** Buffered, queryable telemetry stream. */
|
|
31
|
+
readonly telemetry: TelemetryProbe;
|
|
32
|
+
/**
|
|
33
|
+
* Dispatch an action. Strongly typed against the action's input schema.
|
|
34
|
+
* Returns the action's raw return value (events the handler produced).
|
|
35
|
+
*/
|
|
36
|
+
dispatch<TSchema extends ZodTypeAny>(action: ActionDefinition<TSchema>, input: z.input<TSchema>, envelope?: {
|
|
37
|
+
tenant?: string;
|
|
38
|
+
userId?: string;
|
|
39
|
+
correlationId?: string;
|
|
40
|
+
}): Promise<unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Run a query by reference.
|
|
43
|
+
*/
|
|
44
|
+
query<TResult = unknown>(queryDef: {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
}, input: unknown, tenant?: string): Promise<TResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve when the action/reaction pipeline is quiet — no dispatches in
|
|
49
|
+
* flight, no in-flight reactions, no pending publishes. The harness
|
|
50
|
+
* watches the telemetry stream for an idle window (`graceMs` default
|
|
51
|
+
* 25ms with no new records).
|
|
52
|
+
*/
|
|
53
|
+
idle(graceMs?: number, timeoutMs?: number): Promise<void>;
|
|
54
|
+
/** Stop the underlying app. */
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare function harness(options: HarnessOptions): Promise<Harness>;
|
|
58
|
+
//# sourceMappingURL=harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAa,MAAM,cAAc,CAAC;AACjG,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,cAAc;IAC7B,6BAA6B;IAC7B,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,OAAO;IACtB,iFAAiF;IACjF,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClD,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,OAAO,SAAS,UAAU,EACjC,MAAM,EAAE,gBAAgB,CAAC,OAAO,CAAC,EACjC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,EACvB,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GACtE,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB;;OAEG;IACH,KAAK,CAAC,OAAO,GAAG,OAAO,EACrB,QAAQ,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EACnC,KAAK,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB;;;;;OAKG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,+BAA+B;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAsDvE"}
|
package/dist/harness.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness(app)` — boot a Nwire `AppDefinition` in-process and return a
|
|
3
|
+
* lightweight test handle.
|
|
4
|
+
*
|
|
5
|
+
* const h = await harness({ app: submissionsApp })
|
|
6
|
+
* await h.dispatch(submitAnswer, { … })
|
|
7
|
+
* await h.idle()
|
|
8
|
+
* expect(h.telemetry.count("event.published")).toBeGreaterThan(0)
|
|
9
|
+
* await h.stop()
|
|
10
|
+
*
|
|
11
|
+
* Uses in-memory adapters by default. Pass `providers` to swap in real
|
|
12
|
+
* stores / buses / queues for integration testing.
|
|
13
|
+
*/
|
|
14
|
+
import { TelemetryProbe } from "./telemetry-probe";
|
|
15
|
+
export async function harness(options) {
|
|
16
|
+
const app = options.app.create(options.providers ?? {});
|
|
17
|
+
const probe = new TelemetryProbe();
|
|
18
|
+
const unsubscribe = app.runtime.onTelemetry((rec) => probe.record(rec));
|
|
19
|
+
await app.start();
|
|
20
|
+
let pending = 0;
|
|
21
|
+
let lastRecordAt = Date.now();
|
|
22
|
+
app.runtime.onTelemetry(() => {
|
|
23
|
+
lastRecordAt = Date.now();
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
app,
|
|
27
|
+
telemetry: probe,
|
|
28
|
+
async dispatch(action, input, envelope) {
|
|
29
|
+
pending++;
|
|
30
|
+
try {
|
|
31
|
+
// We seed an envelope from the optional fields. Without parentEnvelope
|
|
32
|
+
// the runtime seeds a fresh one — we let it do that and override
|
|
33
|
+
// tenant/userId via the runtime's envelope hooks instead (when needed).
|
|
34
|
+
// For typical tests, callers pass {tenant, userId} directly.
|
|
35
|
+
if (envelope) {
|
|
36
|
+
const { seedEnvelope } = await import("@nwire/envelope");
|
|
37
|
+
const seeded = seedEnvelope({
|
|
38
|
+
tenant: envelope.tenant,
|
|
39
|
+
userId: envelope.userId,
|
|
40
|
+
correlationId: envelope.correlationId,
|
|
41
|
+
});
|
|
42
|
+
return await app.runtime.dispatch(action, input, seeded);
|
|
43
|
+
}
|
|
44
|
+
return await app.runtime.dispatch(action, input);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
pending--;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async query(queryDef, input, tenant = "") {
|
|
51
|
+
return app.runtime.query(queryDef.name, input, tenant);
|
|
52
|
+
},
|
|
53
|
+
async idle(graceMs = 25, timeoutMs = 5000) {
|
|
54
|
+
const deadline = Date.now() + timeoutMs;
|
|
55
|
+
while (Date.now() < deadline) {
|
|
56
|
+
if (pending === 0 && Date.now() - lastRecordAt > graceMs)
|
|
57
|
+
return;
|
|
58
|
+
await sleep(10);
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`harness.idle: pipeline did not settle within ${timeoutMs}ms (pending=${pending})`);
|
|
61
|
+
},
|
|
62
|
+
async stop() {
|
|
63
|
+
unsubscribe();
|
|
64
|
+
await app.stop();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.js","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AA6CnD,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,OAAuB;IACnD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,IAAI,cAAc,EAAE,CAAC;IACnC,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAc,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACnF,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IAElB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC9B,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;QAC3B,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,GAAG;QACH,SAAS,EAAE,KAAK;QAChB,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ;YACpC,OAAO,EAAE,CAAC;YACV,IAAI,CAAC;gBACH,uEAAuE;gBACvE,iEAAiE;gBACjE,wEAAwE;gBACxE,6DAA6D;gBAC7D,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;oBACzD,MAAM,MAAM,GAAG,YAAY,CAAC;wBAC1B,MAAM,EAAE,QAAQ,CAAC,MAAM;wBACvB,MAAM,EAAE,QAAQ,CAAC,MAAM;wBACvB,aAAa,EAAE,QAAQ,CAAC,aAAa;qBACtC,CAAC,CAAC;oBACH,OAAO,MAAM,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;gBAC3D,CAAC;gBACD,OAAO,MAAM,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YACnD,CAAC;oBAAS,CAAC;gBACT,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QACD,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,EAAE;YACtC,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QACzD,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,EAAE,EAAE,SAAS,GAAG,IAAI;YACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;YACxC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;gBAC7B,IAAI,OAAO,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,GAAG,OAAO;oBAAE,OAAO;gBACjE,MAAM,KAAK,CAAC,EAAE,CAAC,CAAC;YAClB,CAAC;YACD,MAAM,IAAI,KAAK,CACb,gDAAgD,SAAS,eAAe,OAAO,GAAG,CACnF,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI;YACR,WAAW,EAAE,CAAC;YACd,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `isReachable(host, port, timeoutMs)` — single TCP probe with a fixed
|
|
3
|
+
* timeout. Returns `true` if a connection can be established, `false`
|
|
4
|
+
* otherwise.
|
|
5
|
+
*
|
|
6
|
+
* Used by integration tests to gate `describe.skipIf(!reachable)`: when
|
|
7
|
+
* docker-compose isn't running, the whole suite skips with a clean
|
|
8
|
+
* vitest "skipped" status — no spurious connection errors, no fake
|
|
9
|
+
* passes. Production-real tests run when the stack is up, get out of
|
|
10
|
+
* the way otherwise.
|
|
11
|
+
*
|
|
12
|
+
* Why a tiny dedicated helper instead of `waitForTcp`:
|
|
13
|
+
*
|
|
14
|
+
* - `waitForTcp` (in docker-compose.ts) is a poll loop that THROWS on
|
|
15
|
+
* timeout — meant for "I just brought this up, wait until ready."
|
|
16
|
+
* - `isReachable` is a single-shot probe that returns a boolean —
|
|
17
|
+
* meant for "is the dev stack running RIGHT NOW?"
|
|
18
|
+
*
|
|
19
|
+
* Two different intents; two different shapes. The default timeout is
|
|
20
|
+
* short (1s) because we don't want gated tests to hang CI when the
|
|
21
|
+
* service is genuinely down.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isReachable(host: string, port: number, timeoutMs?: number): Promise<boolean>;
|
|
24
|
+
//# sourceMappingURL=is-reachable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-reachable.d.ts","sourceRoot":"","sources":["../src/is-reachable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBjG"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `isReachable(host, port, timeoutMs)` — single TCP probe with a fixed
|
|
3
|
+
* timeout. Returns `true` if a connection can be established, `false`
|
|
4
|
+
* otherwise.
|
|
5
|
+
*
|
|
6
|
+
* Used by integration tests to gate `describe.skipIf(!reachable)`: when
|
|
7
|
+
* docker-compose isn't running, the whole suite skips with a clean
|
|
8
|
+
* vitest "skipped" status — no spurious connection errors, no fake
|
|
9
|
+
* passes. Production-real tests run when the stack is up, get out of
|
|
10
|
+
* the way otherwise.
|
|
11
|
+
*
|
|
12
|
+
* Why a tiny dedicated helper instead of `waitForTcp`:
|
|
13
|
+
*
|
|
14
|
+
* - `waitForTcp` (in docker-compose.ts) is a poll loop that THROWS on
|
|
15
|
+
* timeout — meant for "I just brought this up, wait until ready."
|
|
16
|
+
* - `isReachable` is a single-shot probe that returns a boolean —
|
|
17
|
+
* meant for "is the dev stack running RIGHT NOW?"
|
|
18
|
+
*
|
|
19
|
+
* Two different intents; two different shapes. The default timeout is
|
|
20
|
+
* short (1s) because we don't want gated tests to hang CI when the
|
|
21
|
+
* service is genuinely down.
|
|
22
|
+
*/
|
|
23
|
+
export async function isReachable(host, port, timeoutMs = 1_000) {
|
|
24
|
+
const net = await import("node:net");
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const socket = new net.Socket();
|
|
27
|
+
let done = false;
|
|
28
|
+
const finish = (ok) => {
|
|
29
|
+
if (done)
|
|
30
|
+
return;
|
|
31
|
+
done = true;
|
|
32
|
+
socket.destroy();
|
|
33
|
+
resolve(ok);
|
|
34
|
+
};
|
|
35
|
+
socket.setTimeout(timeoutMs);
|
|
36
|
+
socket
|
|
37
|
+
.once("connect", () => finish(true))
|
|
38
|
+
.once("error", () => finish(false))
|
|
39
|
+
.once("timeout", () => finish(false))
|
|
40
|
+
.connect(port, host);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=is-reachable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-reachable.js","sourceRoot":"","sources":["../src/is-reachable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,IAAY,EAAE,SAAS,GAAG,KAAK;IAC7E,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;IACrC,OAAO,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;QACtC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;QAChC,IAAI,IAAI,GAAG,KAAK,CAAC;QACjB,MAAM,MAAM,GAAG,CAAC,EAAW,EAAE,EAAE;YAC7B,IAAI,IAAI;gBAAE,OAAO;YACjB,IAAI,GAAG,IAAI,CAAC;YACZ,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC,CAAC;QACF,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAC7B,MAAM;aACH,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;aACnC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;aAClC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;aACpC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Agent } from "supertest";
|
|
2
|
+
import type Koa from "koa";
|
|
3
|
+
export declare function createTestApp(app: Koa): Agent;
|
|
4
|
+
export interface BootedTestApp<TBootResult> {
|
|
5
|
+
boot: TBootResult;
|
|
6
|
+
request: Agent;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Smoke-test helper: boot a service and return both the boot result and a
|
|
10
|
+
* supertest agent. Saves three lines of setup per test file.
|
|
11
|
+
*
|
|
12
|
+
* const { request } = await bootTestApp(boot)
|
|
13
|
+
* const res = await request.post('/enrollments').send({ ... })
|
|
14
|
+
*/
|
|
15
|
+
export declare function bootTestApp<TBootResult extends {
|
|
16
|
+
app: Koa;
|
|
17
|
+
}>(bootFn: () => Promise<TBootResult>): Promise<BootedTestApp<TBootResult>>;
|
|
18
|
+
//# sourceMappingURL=supertest-app-helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supertest-app-helper.d.ts","sourceRoot":"","sources":["../src/supertest-app-helper.ts"],"names":[],"mappings":"AAAA,OAAkB,EAAE,KAAK,KAAK,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAE3B,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,KAAK,CAE7C;AAED,MAAM,WAAW,aAAa,CAAC,WAAW;IACxC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,KAAK,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,WAAW,SAAS;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,EAChE,MAAM,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,GACjC,OAAO,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAMrC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import supertest from "supertest";
|
|
2
|
+
export function createTestApp(app) {
|
|
3
|
+
return supertest(app.callback());
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Smoke-test helper: boot a service and return both the boot result and a
|
|
7
|
+
* supertest agent. Saves three lines of setup per test file.
|
|
8
|
+
*
|
|
9
|
+
* const { request } = await bootTestApp(boot)
|
|
10
|
+
* const res = await request.post('/enrollments').send({ ... })
|
|
11
|
+
*/
|
|
12
|
+
export async function bootTestApp(bootFn) {
|
|
13
|
+
const result = await bootFn();
|
|
14
|
+
return {
|
|
15
|
+
boot: result,
|
|
16
|
+
request: createTestApp(result.app),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=supertest-app-helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supertest-app-helper.js","sourceRoot":"","sources":["../src/supertest-app-helper.ts"],"names":[],"mappings":"AAAA,OAAO,SAAyB,MAAM,WAAW,CAAC;AAGlD,MAAM,UAAU,aAAa,CAAC,GAAQ;IACpC,OAAO,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;AACnC,CAAC;AAOD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAkC;IAElC,MAAM,MAAM,GAAG,MAAM,MAAM,EAAE,CAAC;IAC9B,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC;KACnC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TelemetryProbe` — buffered, queryable view of `runtime.onTelemetry`.
|
|
3
|
+
*
|
|
4
|
+
* Used by the harness; can also be attached standalone:
|
|
5
|
+
*
|
|
6
|
+
* const probe = new TelemetryProbe()
|
|
7
|
+
* runtime.onTelemetry(rec => probe.record(rec))
|
|
8
|
+
*
|
|
9
|
+
* probe.count("event.published", e => e.event.eventName === "foo")
|
|
10
|
+
* probe.chain("foo") // forward causal chain from event named 'foo'
|
|
11
|
+
* probe.drift(declaredPairs)
|
|
12
|
+
*/
|
|
13
|
+
import type { Telemetry } from "@nwire/forge";
|
|
14
|
+
export type TelemetryFilter<K extends Telemetry["kind"] = Telemetry["kind"]> = (rec: Extract<Telemetry, {
|
|
15
|
+
kind: K;
|
|
16
|
+
}>) => boolean;
|
|
17
|
+
export declare class TelemetryProbe {
|
|
18
|
+
private readonly buffer;
|
|
19
|
+
/** Hard cap so a long-running test doesn't OOM. Default 10k. */
|
|
20
|
+
bufferCap: number;
|
|
21
|
+
record(rec: Telemetry): void;
|
|
22
|
+
/** Drop everything (e.g. between scenarios). */
|
|
23
|
+
reset(): void;
|
|
24
|
+
/** Raw access for advanced queries. */
|
|
25
|
+
all(): readonly Telemetry[];
|
|
26
|
+
/** Records of a single kind. */
|
|
27
|
+
ofKind<K extends Telemetry["kind"]>(kind: K): ReadonlyArray<Extract<Telemetry, {
|
|
28
|
+
kind: K;
|
|
29
|
+
}>>;
|
|
30
|
+
/** Count records of a kind, optionally matching a predicate. */
|
|
31
|
+
count<K extends Telemetry["kind"]>(kind: K, predicate?: TelemetryFilter<K>): number;
|
|
32
|
+
/**
|
|
33
|
+
* Walk one causal correlation chain, returning the records ordered by
|
|
34
|
+
* `ts` (causal forward). `entry` is either a `correlationId` string or
|
|
35
|
+
* an event name (in which case the first observed correlation is used).
|
|
36
|
+
*/
|
|
37
|
+
chain(entry: string): readonly Telemetry[];
|
|
38
|
+
/**
|
|
39
|
+
* Observed event pair counts (parent.eventName → child.eventName).
|
|
40
|
+
* Built from `event.published` records keyed by `causationId` chains
|
|
41
|
+
* within each correlationId.
|
|
42
|
+
*/
|
|
43
|
+
observedPairs(): Map<string, number>;
|
|
44
|
+
/**
|
|
45
|
+
* Compare observed pair counts vs a declared set; returns drift entries
|
|
46
|
+
* (observed but not in declared).
|
|
47
|
+
*/
|
|
48
|
+
drift(declared: ReadonlySet<string>): Array<{
|
|
49
|
+
pair: string;
|
|
50
|
+
count: number;
|
|
51
|
+
}>;
|
|
52
|
+
/** Errors observed in any *.failed kind. */
|
|
53
|
+
errors(): readonly Telemetry[];
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=telemetry-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-probe.d.ts","sourceRoot":"","sources":["../src/telemetry-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAC7E,GAAG,EAAE,OAAO,CAAC,SAAS,EAAE;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,KACjC,OAAO,CAAC;AAEb,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,gEAAgE;IAChE,SAAS,SAAU;IAEnB,MAAM,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI;IAK5B,gDAAgD;IAChD,KAAK,IAAI,IAAI;IAIb,uCAAuC;IACvC,GAAG,IAAI,SAAS,SAAS,EAAE;IAI3B,gCAAgC;IAChC,MAAM,CAAC,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,SAAS,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IAI5F,gEAAgE;IAChE,KAAK,CAAC,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,MAAM;IAKnF;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE;IAqB1C;;;;OAIG;IACH,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAuBpC;;;OAGG;IACH,KAAK,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAS5E,4CAA4C;IAC5C,MAAM,IAAI,SAAS,SAAS,EAAE;CAS/B"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TelemetryProbe` — buffered, queryable view of `runtime.onTelemetry`.
|
|
3
|
+
*
|
|
4
|
+
* Used by the harness; can also be attached standalone:
|
|
5
|
+
*
|
|
6
|
+
* const probe = new TelemetryProbe()
|
|
7
|
+
* runtime.onTelemetry(rec => probe.record(rec))
|
|
8
|
+
*
|
|
9
|
+
* probe.count("event.published", e => e.event.eventName === "foo")
|
|
10
|
+
* probe.chain("foo") // forward causal chain from event named 'foo'
|
|
11
|
+
* probe.drift(declaredPairs)
|
|
12
|
+
*/
|
|
13
|
+
export class TelemetryProbe {
|
|
14
|
+
buffer = [];
|
|
15
|
+
/** Hard cap so a long-running test doesn't OOM. Default 10k. */
|
|
16
|
+
bufferCap = 10_000;
|
|
17
|
+
record(rec) {
|
|
18
|
+
this.buffer.push(rec);
|
|
19
|
+
if (this.buffer.length > this.bufferCap)
|
|
20
|
+
this.buffer.shift();
|
|
21
|
+
}
|
|
22
|
+
/** Drop everything (e.g. between scenarios). */
|
|
23
|
+
reset() {
|
|
24
|
+
this.buffer.length = 0;
|
|
25
|
+
}
|
|
26
|
+
/** Raw access for advanced queries. */
|
|
27
|
+
all() {
|
|
28
|
+
return this.buffer;
|
|
29
|
+
}
|
|
30
|
+
/** Records of a single kind. */
|
|
31
|
+
ofKind(kind) {
|
|
32
|
+
return this.buffer.filter((r) => r.kind === kind);
|
|
33
|
+
}
|
|
34
|
+
/** Count records of a kind, optionally matching a predicate. */
|
|
35
|
+
count(kind, predicate) {
|
|
36
|
+
const matches = this.ofKind(kind);
|
|
37
|
+
return predicate ? matches.filter(predicate).length : matches.length;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Walk one causal correlation chain, returning the records ordered by
|
|
41
|
+
* `ts` (causal forward). `entry` is either a `correlationId` string or
|
|
42
|
+
* an event name (in which case the first observed correlation is used).
|
|
43
|
+
*/
|
|
44
|
+
chain(entry) {
|
|
45
|
+
let correlationId = entry;
|
|
46
|
+
if (!entry.includes("-")) {
|
|
47
|
+
// Probably an event name — find the first record matching it.
|
|
48
|
+
const first = this.buffer.find((r) => r.kind === "event.published" && r.event.eventName === entry);
|
|
49
|
+
if (!first)
|
|
50
|
+
return [];
|
|
51
|
+
correlationId = first.envelope.correlationId;
|
|
52
|
+
}
|
|
53
|
+
return this.buffer
|
|
54
|
+
.filter((r) => {
|
|
55
|
+
if (!("envelope" in r))
|
|
56
|
+
return false;
|
|
57
|
+
const env = r.envelope;
|
|
58
|
+
return !!env && env.correlationId === correlationId;
|
|
59
|
+
})
|
|
60
|
+
.slice()
|
|
61
|
+
.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Observed event pair counts (parent.eventName → child.eventName).
|
|
65
|
+
* Built from `event.published` records keyed by `causationId` chains
|
|
66
|
+
* within each correlationId.
|
|
67
|
+
*/
|
|
68
|
+
observedPairs() {
|
|
69
|
+
const events = [...this.ofKind("event.published")];
|
|
70
|
+
const byCorr = new Map();
|
|
71
|
+
for (const e of events) {
|
|
72
|
+
const arr = byCorr.get(e.envelope.correlationId) ?? [];
|
|
73
|
+
arr.push(e);
|
|
74
|
+
byCorr.set(e.envelope.correlationId, arr);
|
|
75
|
+
}
|
|
76
|
+
const out = new Map();
|
|
77
|
+
for (const list of byCorr.values()) {
|
|
78
|
+
list.sort((a, b) => b.ts.localeCompare(a.ts)); // desc — causal order
|
|
79
|
+
for (let i = 1; i < list.length; i++) {
|
|
80
|
+
const parent = list[i - 1].event.eventName;
|
|
81
|
+
const child = list[i].event.eventName;
|
|
82
|
+
if (parent === child)
|
|
83
|
+
continue;
|
|
84
|
+
const k = `${parent}→${child}`;
|
|
85
|
+
out.set(k, (out.get(k) ?? 0) + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Compare observed pair counts vs a declared set; returns drift entries
|
|
92
|
+
* (observed but not in declared).
|
|
93
|
+
*/
|
|
94
|
+
drift(declared) {
|
|
95
|
+
const observed = this.observedPairs();
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const [pair, count] of observed) {
|
|
98
|
+
if (!declared.has(pair))
|
|
99
|
+
out.push({ pair, count });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Errors observed in any *.failed kind. */
|
|
104
|
+
errors() {
|
|
105
|
+
return this.buffer.filter((r) => r.kind === "action.failed" ||
|
|
106
|
+
r.kind === "reaction.failed" ||
|
|
107
|
+
r.kind === "external.call.failed" ||
|
|
108
|
+
r.kind === "dlq.recorded");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=telemetry-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-probe.js","sourceRoot":"","sources":["../src/telemetry-probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nwire/test-kit — test helpers for Nwire apps.
|
|
3
|
+
*
|
|
4
|
+
* Three levels of abstraction:
|
|
5
|
+
*
|
|
6
|
+
* 1. Unit/integration: `harness({ app })` boots an app in-process with
|
|
7
|
+
* in-memory adapters; returns dispatch/query/telemetry/idle/stop.
|
|
8
|
+
*
|
|
9
|
+
* 2. Real-deps integration: `dockerCompose({ preset })` spins up
|
|
10
|
+
* mongo/nats/redis/postgres for the test suite; pass real adapters
|
|
11
|
+
* to the harness via `providers`.
|
|
12
|
+
*
|
|
13
|
+
* 3. BDD: `feature(path, define)` wires Gherkin .feature files to vitest
|
|
14
|
+
* via @amiceli/vitest-cucumber (peer dep).
|
|
15
|
+
*
|
|
16
|
+
* Plus existing helpers: zod fixture factory, supertest agent.
|
|
17
|
+
*/
|
|
18
|
+
export { harness, type Harness, type HarnessOptions } from "./harness";
|
|
19
|
+
export { TelemetryProbe, type TelemetryFilter } from "./telemetry-probe";
|
|
20
|
+
export { dockerCompose, PRESETS as DOCKER_COMPOSE_PRESETS, type ComposeStack, type ComposePreset, } from "./docker-compose";
|
|
21
|
+
export { isReachable } from "./is-reachable";
|
|
22
|
+
export { feature, type BddContext } from "./bdd";
|
|
23
|
+
export { factory, sequence, type Factory, type SequenceCounter } from "./zod-fixture-factory";
|
|
24
|
+
export { createTestApp, bootTestApp, type BootedTestApp } from "./supertest-app-helper";
|
|
25
|
+
//# sourceMappingURL=test-kit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-kit.d.ts","sourceRoot":"","sources":["../src/test-kit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,aAAa,EACb,OAAO,IAAI,sBAAsB,EACjC,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC;AAGjD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC9F,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/test-kit.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nwire/test-kit — test helpers for Nwire apps.
|
|
3
|
+
*
|
|
4
|
+
* Three levels of abstraction:
|
|
5
|
+
*
|
|
6
|
+
* 1. Unit/integration: `harness({ app })` boots an app in-process with
|
|
7
|
+
* in-memory adapters; returns dispatch/query/telemetry/idle/stop.
|
|
8
|
+
*
|
|
9
|
+
* 2. Real-deps integration: `dockerCompose({ preset })` spins up
|
|
10
|
+
* mongo/nats/redis/postgres for the test suite; pass real adapters
|
|
11
|
+
* to the harness via `providers`.
|
|
12
|
+
*
|
|
13
|
+
* 3. BDD: `feature(path, define)` wires Gherkin .feature files to vitest
|
|
14
|
+
* via @amiceli/vitest-cucumber (peer dep).
|
|
15
|
+
*
|
|
16
|
+
* Plus existing helpers: zod fixture factory, supertest agent.
|
|
17
|
+
*/
|
|
18
|
+
export { harness } from "./harness";
|
|
19
|
+
export { TelemetryProbe } from "./telemetry-probe";
|
|
20
|
+
export { dockerCompose, PRESETS as DOCKER_COMPOSE_PRESETS, } from "./docker-compose";
|
|
21
|
+
export { isReachable } from "./is-reachable";
|
|
22
|
+
export { feature } from "./bdd";
|
|
23
|
+
// Pre-existing helpers
|
|
24
|
+
export { factory, sequence } from "./zod-fixture-factory";
|
|
25
|
+
export { createTestApp, bootTestApp } from "./supertest-app-helper";
|
|
26
|
+
//# sourceMappingURL=test-kit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-kit.js","sourceRoot":"","sources":["../src/test-kit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,OAAO,EAAqC,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,cAAc,EAAwB,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,aAAa,EACb,OAAO,IAAI,sBAAsB,GAGlC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAmB,MAAM,OAAO,CAAC;AAEjD,uBAAuB;AACvB,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAsC,MAAM,uBAAuB,CAAC;AAC9F,OAAO,EAAE,aAAa,EAAE,WAAW,EAAsB,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { ZodTypeAny } from "@nwire/messages";
|
|
3
|
+
export type Factory<T> = (overrides?: Partial<T>) => T;
|
|
4
|
+
/**
|
|
5
|
+
* Build typed, zod-validated fixtures from a schema and a defaults producer.
|
|
6
|
+
*
|
|
7
|
+
* const userFactory = factory(UserSchema, () => ({ id: randomUUID(), role: 'student' }))
|
|
8
|
+
* const u = userFactory({ role: 'teacher' }) // typed; validated; one line per test
|
|
9
|
+
*/
|
|
10
|
+
export declare function factory<TSchema extends ZodTypeAny>(schema: TSchema, defaults: () => z.input<TSchema>): Factory<z.output<TSchema>>;
|
|
11
|
+
export type SequenceCounter = () => number;
|
|
12
|
+
export declare function sequence(start?: number): SequenceCounter;
|
|
13
|
+
//# sourceMappingURL=zod-fixture-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-fixture-factory.d.ts","sourceRoot":"","sources":["../src/zod-fixture-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,OAAO,SAAS,UAAU,EAChD,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAC/B,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAQ5B;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,MAAM,CAAC;AAE3C,wBAAgB,QAAQ,CAAC,KAAK,SAAI,GAAG,eAAe,CAGnD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build typed, zod-validated fixtures from a schema and a defaults producer.
|
|
3
|
+
*
|
|
4
|
+
* const userFactory = factory(UserSchema, () => ({ id: randomUUID(), role: 'student' }))
|
|
5
|
+
* const u = userFactory({ role: 'teacher' }) // typed; validated; one line per test
|
|
6
|
+
*/
|
|
7
|
+
export function factory(schema, defaults) {
|
|
8
|
+
return (overrides) => {
|
|
9
|
+
// Defaults are constrained by TSchema's input type, so spread is safe;
|
|
10
|
+
// zod 4 types this as `unknown` because TSchema is unconstrained at the
|
|
11
|
+
// wildcard, so we widen to a Record for the spread.
|
|
12
|
+
const base = defaults();
|
|
13
|
+
return schema.parse({ ...base, ...overrides });
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function sequence(start = 1) {
|
|
17
|
+
let n = start;
|
|
18
|
+
return () => n++;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=zod-fixture-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-fixture-factory.js","sourceRoot":"","sources":["../src/zod-fixture-factory.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CACrB,MAAe,EACf,QAAgC;IAEhC,OAAO,CAAC,SAAS,EAAE,EAAE;QACnB,uEAAuE;QACvE,wEAAwE;QACxE,oDAAoD;QACpD,MAAM,IAAI,GAAG,QAAQ,EAA6B,CAAC;QACnD,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,EAAE,GAAI,SAAqC,EAAE,CAAC,CAAC;IAC9E,CAAC,CAAC;AACJ,CAAC;AAID,MAAM,UAAU,QAAQ,CAAC,KAAK,GAAG,CAAC;IAChC,IAAI,CAAC,GAAG,KAAK,CAAC;IACd,OAAO,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;AACnB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/test-kit",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Shared test helpers and zod-driven fixture factories",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fixtures",
|
|
7
|
+
"testing",
|
|
8
|
+
"vitest"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/test-kit.js",
|
|
16
|
+
"types": "./dist/test-kit.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": "./dist/test-kit.js",
|
|
20
|
+
"types": "./dist/test-kit.d.ts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"supertest": "^7.2.2",
|
|
28
|
+
"zod": "^4.0.0",
|
|
29
|
+
"@nwire/envelope": "0.7.0",
|
|
30
|
+
"@nwire/messages": "0.7.0",
|
|
31
|
+
"@nwire/forge": "0.7.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/koa": "^2.15.0",
|
|
35
|
+
"@types/node": "^22.19.9",
|
|
36
|
+
"@types/supertest": "^6.0.3",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@amiceli/vitest-cucumber": "^4.0.0",
|
|
41
|
+
"koa": "^2.16.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"koa": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@amiceli/vitest-cucumber": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
"dev": "tsc --watch",
|
|
54
|
+
"typecheck": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|