@nwire/test-kit 0.12.0 → 0.13.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.
@@ -19,7 +19,7 @@
19
19
  * and the optional deps (`@nwire/auth`, `@nwire/rbac`, `koa`) stay opt-in.
20
20
  */
21
21
  import supertest from "supertest";
22
- import { forgeDispatcher } from "@nwire/forge";
22
+ import { FORGE_ACTION_RUNNER_BINDING, FORGE_QUERY_RUNNER_BINDING, } from "@nwire/forge";
23
23
  /**
24
24
  * Return a thin proxy that pins `envelope.user` (and `envelope.userId`,
25
25
  * `envelope.tenant` derived from the user) on every dispatch/query.
@@ -33,11 +33,12 @@ export function asUser(harness, user) {
33
33
  // `user` too, which is on MessageEnvelope but not on the public dispatch
34
34
  // overload. Going through runtime.dispatch with a hand-seeded envelope
35
35
  // keeps the type contract honest without widening the public Harness API.
36
- const dispatcher = forgeDispatcher(harness.app);
36
+ const actionRunner = harness.app.container.resolve(FORGE_ACTION_RUNNER_BINDING);
37
+ const queryRunner = harness.app.container.resolve(FORGE_QUERY_RUNNER_BINDING);
37
38
  return {
38
39
  async dispatch(action, input, envelope) {
39
40
  if (!user)
40
- return dispatcher.dispatch(action, input);
41
+ return actionRunner.dispatch(action, input);
41
42
  const { seedEnvelope } = await import("@nwire/envelope");
42
43
  const seeded = seedEnvelope({
43
44
  tenant: envelope?.tenant ?? user.tenant,
@@ -45,10 +46,10 @@ export function asUser(harness, user) {
45
46
  user,
46
47
  correlationId: envelope?.correlationId,
47
48
  });
48
- return dispatcher.dispatch(action, input, seeded);
49
+ return actionRunner.dispatch(action, input, seeded);
49
50
  },
50
51
  async query(queryDef, input, tenant) {
51
- return dispatcher.query(queryDef.name, input, tenant ?? user?.tenant ?? "");
52
+ return queryRunner.run(queryDef.name, input, tenant ?? user?.tenant ?? "");
52
53
  },
53
54
  };
54
55
  }
package/dist/harness.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * dispatch helpers, idle-wait, and a buffered telemetry probe.
4
4
  *
5
5
  * const app = createApp({ appName: "submissions",
6
- * plugins: [createForgePlugin({ handlers, actors })] });
6
+ * handlers, plugins: [...forgePlugins({ actors })] });
7
7
  * const h = await harness({ app });
8
8
  * await h.dispatch(submitAnswer, { … });
9
9
  * await h.idle();
package/dist/harness.js CHANGED
@@ -3,21 +3,22 @@
3
3
  * dispatch helpers, idle-wait, and a buffered telemetry probe.
4
4
  *
5
5
  * const app = createApp({ appName: "submissions",
6
- * plugins: [createForgePlugin({ handlers, actors })] });
6
+ * handlers, plugins: [...forgePlugins({ actors })] });
7
7
  * const h = await harness({ app });
8
8
  * await h.dispatch(submitAnswer, { … });
9
9
  * await h.idle();
10
10
  * expect(h.telemetry.count("event.published")).toBeGreaterThan(0);
11
11
  * await h.stop();
12
12
  */
13
- import { forgeDispatcher } from "@nwire/forge";
13
+ import { FORGE_ACTION_RUNNER_BINDING, FORGE_QUERY_RUNNER_BINDING } from "@nwire/forge";
14
14
  import { TelemetryProbe } from "./telemetry-probe.js";
15
15
  export async function harness(options) {
16
16
  const app = options.app;
17
17
  const probe = new TelemetryProbe();
18
18
  const unsubscribe = app.runtime.onTelemetry((rec) => probe.record(rec));
19
19
  await app.start();
20
- const dispatcher = forgeDispatcher(app);
20
+ const actionRunner = app.container.resolve(FORGE_ACTION_RUNNER_BINDING);
21
+ const queryRunner = app.container.resolve(FORGE_QUERY_RUNNER_BINDING);
21
22
  let pending = 0;
22
23
  let lastRecordAt = Date.now();
23
24
  app.runtime.onTelemetry(() => {
@@ -36,16 +37,16 @@ export async function harness(options) {
36
37
  userId: envelope.userId,
37
38
  correlationId: envelope.correlationId,
38
39
  });
39
- return await dispatcher.dispatch(action, input, seeded);
40
+ return await actionRunner.dispatch(action, input, seeded);
40
41
  }
41
- return await dispatcher.dispatch(action, input);
42
+ return await actionRunner.dispatch(action, input);
42
43
  }
43
44
  finally {
44
45
  pending--;
45
46
  }
46
47
  },
47
48
  async query(queryDef, input, tenant = "") {
48
- return dispatcher.query(queryDef.name, input, tenant);
49
+ return queryRunner.run(queryDef.name, input, tenant);
49
50
  },
50
51
  async idle(graceMs = 25, timeoutMs = 5000) {
51
52
  const deadline = Date.now() + timeoutMs;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `invoke(handler, input)` / `invokeQuery(query, input)` — exercise a single
3
+ * handler or query without hand-assembling an app or a ctx.
4
+ *
5
+ * A read or a command always needs a ctx (container + tenant + observability),
6
+ * so there is no ctx-less call path in production — but in a unit test you
7
+ * shouldn't have to build one. `invoke` boots a minimal in-memory forge app
8
+ * around just your handler (plus any deps it reaches for), dispatches once
9
+ * through the real pipeline, returns the result, and tears the app down.
10
+ *
11
+ * const ev = await invoke(submitAnswer, { id: "a-1" });
12
+ * const user = await invoke(createUser, { email }, { providers: { db } });
13
+ * const rows = await invokeQuery(listUsers, { tenantId }, { providers: { db } });
14
+ *
15
+ * Pass `actors` / `projections` / `queries` / `handlers` when the handler
16
+ * reaches for them via `ctx.actor` / `ctx.query` / `ctx.send`; pass `providers`
17
+ * for DI bindings it resolves. For multi-dispatch scenarios or telemetry
18
+ * assertions, use `harness({ app })` instead.
19
+ */
20
+ import { type ActionDefinition, type ActionInput, type ActionResult, type QueryDefinition } from "@nwire/forge";
21
+ import type { ZodTypeAny } from "@nwire/messages";
22
+ export interface InvokeOptions {
23
+ /** DI bindings the handler resolves via `ctx.resolve(name)`. */
24
+ readonly providers?: Record<string, unknown>;
25
+ /** Extra handlers the target reaches via `ctx.send` / `ctx.execute`. */
26
+ readonly handlers?: readonly ActionDefinition<any>[];
27
+ /** Actors the target reads via `ctx.actor`. */
28
+ readonly actors?: readonly any[];
29
+ /** Projections backing queries / folded by emitted events. */
30
+ readonly projections?: readonly any[];
31
+ /** Queries the target reads via `ctx.query`. */
32
+ readonly queries?: readonly QueryDefinition<any, any, any>[];
33
+ /** Envelope seed — tenant / userId / correlationId for the dispatch. */
34
+ readonly envelope?: {
35
+ tenant?: string;
36
+ userId?: string;
37
+ correlationId?: string;
38
+ };
39
+ }
40
+ /**
41
+ * Boot a minimal app around `handler`, dispatch `input` once through the real
42
+ * forge pipeline (validation, hooks, retry, event publish), return the
43
+ * handler's result, and stop the app.
44
+ */
45
+ export declare function invoke<TSchema extends ZodTypeAny>(action: ActionDefinition<TSchema>, input: ActionInput<ActionDefinition<TSchema>>, opts?: InvokeOptions): Promise<ActionResult<ActionDefinition<TSchema>>>;
46
+ /**
47
+ * Boot a minimal app around `query`, run it once (projection- or handler-form),
48
+ * return the result, and stop the app. Pass `projections` for a projection-
49
+ * backed query and `providers` for a handler-form query's DI deps.
50
+ */
51
+ export declare function invokeQuery<TResult = unknown>(query: QueryDefinition<any, any, TResult>, input: unknown, opts?: InvokeOptions): Promise<TResult>;
package/dist/invoke.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `invoke(handler, input)` / `invokeQuery(query, input)` — exercise a single
3
+ * handler or query without hand-assembling an app or a ctx.
4
+ *
5
+ * A read or a command always needs a ctx (container + tenant + observability),
6
+ * so there is no ctx-less call path in production — but in a unit test you
7
+ * shouldn't have to build one. `invoke` boots a minimal in-memory forge app
8
+ * around just your handler (plus any deps it reaches for), dispatches once
9
+ * through the real pipeline, returns the result, and tears the app down.
10
+ *
11
+ * const ev = await invoke(submitAnswer, { id: "a-1" });
12
+ * const user = await invoke(createUser, { email }, { providers: { db } });
13
+ * const rows = await invokeQuery(listUsers, { tenantId }, { providers: { db } });
14
+ *
15
+ * Pass `actors` / `projections` / `queries` / `handlers` when the handler
16
+ * reaches for them via `ctx.actor` / `ctx.query` / `ctx.send`; pass `providers`
17
+ * for DI bindings it resolves. For multi-dispatch scenarios or telemetry
18
+ * assertions, use `harness({ app })` instead.
19
+ */
20
+ import { createApp } from "@nwire/app";
21
+ import { forgePlugins, FORGE_ACTION_RUNNER_BINDING, FORGE_QUERY_RUNNER_BINDING, } from "@nwire/forge";
22
+ function providerPlugin(providers) {
23
+ return {
24
+ name: "test.providers",
25
+ setup({ bind }) {
26
+ for (const [name, value] of Object.entries(providers ?? {})) {
27
+ bind(name, value);
28
+ }
29
+ },
30
+ };
31
+ }
32
+ function buildApp(target, opts, asQuery) {
33
+ return createApp({
34
+ appName: "invoke",
35
+ handlers: [
36
+ ...(!asQuery ? [target] : []),
37
+ ...(opts.handlers ?? []),
38
+ ...(asQuery ? [target] : []),
39
+ ...(opts.queries ?? []),
40
+ ],
41
+ plugins: [
42
+ providerPlugin(opts.providers),
43
+ ...forgePlugins({
44
+ actors: opts.actors,
45
+ projections: opts.projections,
46
+ }),
47
+ ],
48
+ });
49
+ }
50
+ /**
51
+ * Boot a minimal app around `handler`, dispatch `input` once through the real
52
+ * forge pipeline (validation, hooks, retry, event publish), return the
53
+ * handler's result, and stop the app.
54
+ */
55
+ export async function invoke(action, input, opts = {}) {
56
+ const app = buildApp(action, opts, false);
57
+ await app.start();
58
+ try {
59
+ const runner = app.container.resolve(FORGE_ACTION_RUNNER_BINDING);
60
+ if (opts.envelope) {
61
+ const { seedEnvelope } = await import("@nwire/envelope");
62
+ return (await runner.dispatch(action, input, seedEnvelope(opts.envelope)));
63
+ }
64
+ return (await runner.dispatch(action, input));
65
+ }
66
+ finally {
67
+ await app.stop();
68
+ }
69
+ }
70
+ /**
71
+ * Boot a minimal app around `query`, run it once (projection- or handler-form),
72
+ * return the result, and stop the app. Pass `projections` for a projection-
73
+ * backed query and `providers` for a handler-form query's DI deps.
74
+ */
75
+ export async function invokeQuery(
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ query, input, opts = {}) {
78
+ const app = buildApp(query, opts, true);
79
+ await app.start();
80
+ try {
81
+ const runner = app.container.resolve(FORGE_QUERY_RUNNER_BINDING);
82
+ return await runner.run(query.name, input, opts.envelope?.tenant ?? "");
83
+ }
84
+ finally {
85
+ await app.stop();
86
+ }
87
+ }
@@ -16,6 +16,7 @@
16
16
  * Plus existing helpers: zod fixture factory, supertest agent.
17
17
  */
18
18
  export { harness, type Harness, type HarnessOptions } from "./harness.js";
19
+ export { invoke, invokeQuery, type InvokeOptions } from "./invoke.js";
19
20
  export { asUser, simulateRequest, checkPolicy, onLog, type UserLike, type ScopedHarness, type SimulatedRequest, type SimulatedResponse, type PolicyCheck, type LogLevel, type LogEntry, } from "./harness-extensions.js";
20
21
  export { TelemetryProbe, type TelemetryFilter } from "./telemetry-probe.js";
21
22
  export { dockerCompose, PRESETS as DOCKER_COMPOSE_PRESETS, type ComposeStack, type ComposePreset, } from "./docker-compose.js";
package/dist/test-kit.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * Plus existing helpers: zod fixture factory, supertest agent.
17
17
  */
18
18
  export { harness } from "./harness.js";
19
+ export { invoke, invokeQuery } from "./invoke.js";
19
20
  export { asUser, simulateRequest, checkPolicy, onLog, } from "./harness-extensions.js";
20
21
  export { TelemetryProbe } from "./telemetry-probe.js";
21
22
  export { dockerCompose, PRESETS as DOCKER_COMPOSE_PRESETS, } from "./docker-compose.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwire/test-kit",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Shared test helpers and zod-driven fixture factories",
5
5
  "keywords": [
6
6
  "fixtures",
@@ -28,20 +28,20 @@
28
28
  "dependencies": {
29
29
  "supertest": "^7.2.2",
30
30
  "zod": "^4.0.0",
31
- "@nwire/envelope": "0.12.0",
32
- "@nwire/logger": "0.12.0",
33
- "@nwire/messages": "0.12.0",
34
- "@nwire/forge": "0.12.0"
31
+ "@nwire/logger": "0.13.0",
32
+ "@nwire/messages": "0.13.0",
33
+ "@nwire/envelope": "0.13.0",
34
+ "@nwire/forge": "0.13.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/koa": "^2.15.0",
38
38
  "@types/node": "^22.19.9",
39
39
  "@types/supertest": "^6.0.3",
40
40
  "typescript": "^5.9.3",
41
- "@nwire/app": "0.12.0",
42
- "@nwire/endpoint": "0.12.0",
43
- "@nwire/koa": "0.12.0",
44
- "@nwire/wires": "0.12.0"
41
+ "@nwire/app": "0.13.0",
42
+ "@nwire/endpoint": "0.13.0",
43
+ "@nwire/koa": "0.13.0",
44
+ "@nwire/wires": "0.13.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@amiceli/vitest-cucumber": "^4.0.0",