@zhin.js/testing 1.0.6 → 1.0.8

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/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { realpath } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
- import { defineAdapter, type AdapterDefinition, type AdapterSegmentPolicy,
3
+ import { Adapter, createEndpointSpecification, endpointSpec, defineAdapter, type Endpoint, type AdapterDefinition, type AdapterSegmentPolicy,
4
4
  type EndpointIdentity, type EndpointEventSink, type EndpointIncomingMessage, type EndpointSendRequest } from 'zhin.js/adapter';
5
5
  import { ImRuntime, type ConversationAddress, type MessageDispatchResult,
6
6
  type RuntimeMessageDiagnosticsReader, type SendContent } from 'zhin.js/core/runtime';
@@ -16,9 +16,10 @@ export { FakeDatabaseRootHost, FakeOutboundRootHost, type FakeOutboundCall } fro
16
16
  export { FakePlugin, createFakePlugin, runPluginSetup, resolvePluginConfig, PluginConfigValidationError, fakeResource,
17
17
  type FakePluginOptions, type RunPluginSetupOptions, type FakeResource, type FakeRegisteredFeature } from './fake-plugin.js';
18
18
  export { fakeOf } from './partial.js';
19
- export { createFakeMessage, createCommandContext, runCommand, runMiddleware, runHandler,
19
+ export { createFakeMessage, createFakeInteraction, createCommandContext, runCommand, runMiddleware, runHandler,
20
20
  createToolContext, runTool, toolOriginFromMessage,
21
- type FakeMessage, type RunToolOptions, type FakeMessageInit, type RunCommandOptions, type RunMiddlewareResult } from './fake-runners.js';
21
+ type FakeMessage, type FakeSentDelivery, type FakeInteraction, type FakeInteractionOptions,
22
+ type RunToolOptions, type FakeMessageInit, type RunCommandOptions, type RunMiddlewareResult } from './fake-runners.js';
22
23
 
23
24
  export interface PluginTestKitOptions {
24
25
  /** A real, disposable test app with its own package.json, plugin entry, dependencies and mounts. */
@@ -80,27 +81,55 @@ export class PluginTestKit {
80
81
  || names.some(name => !/^[a-z][a-z0-9-]*$/.test(name))) {
81
82
  throw new TypeError('Test endpoints must have unique lowercase names');
82
83
  }
83
- this.#defaultEndpoint = names[0]!;
84
- const definitions = new Map<string, AdapterDefinition>(names.map(name => [name, defineAdapter({
85
- capabilities: ['inbound', 'outbound'],
86
- segments: options.segments ?? { html: 'text', interactive: 'text', markdown: 'text' },
87
- create: () => ({
88
- client: Object.freeze({ synthetic: true }),
89
- connect: ({ events, identity }) => {
90
- this.#identities.set(name, identity);
91
- this.#sinks.set(name, events);
92
- return () => { if (this.#sinks.get(name) === events) { this.#sinks.delete(name); this.#identities.delete(name); } };
93
- },
94
- send: (request: EndpointSendRequest) => {
95
- const id = `test-${this.#nextDelivery++}`;
96
- this.#attempts.push(Object.freeze({ endpoint: name, id,
97
- conversation: captureDeliveryValue(request.conversation),
98
- payload: captureDeliveryValue(request.payload),
99
- }));
100
- return id;
101
- },
102
- }),
103
- })]));
84
+ const [firstEndpoint = 'test'] = names;
85
+ this.#defaultEndpoint = firstEndpoint;
86
+ const recordAttempt = (endpoint: string, request: EndpointSendRequest): string => {
87
+ const id = `test-${this.#nextDelivery++}`;
88
+ this.#attempts.push(Object.freeze({
89
+ endpoint,
90
+ id,
91
+ conversation: captureDeliveryValue(request.conversation),
92
+ payload: captureDeliveryValue(request.payload),
93
+ }));
94
+ return id;
95
+ };
96
+ const registerOpen = (endpoint: string, spec: Endpoint[typeof endpointSpec]): void => {
97
+ this.#identities.set(endpoint, spec.identity);
98
+ this.#sinks.set(endpoint, spec.events);
99
+ };
100
+ const releaseEndpoint = (endpoint: string, spec: Endpoint[typeof endpointSpec]): void => {
101
+ if (this.#sinks.get(endpoint) !== spec.events) return;
102
+ this.#sinks.delete(endpoint);
103
+ this.#identities.delete(endpoint);
104
+ };
105
+ const definitions = new Map<string, AdapterDefinition>(names.map(name => {
106
+ class SyntheticClient {
107
+ readonly synthetic = true;
108
+ send(request: EndpointSendRequest): string {
109
+ return recordAttempt(name, request);
110
+ }
111
+ }
112
+ class SyntheticEndpoint extends SyntheticClient implements Endpoint {
113
+ readonly [endpointSpec] = createEndpointSpecification(this, {
114
+ start: signal => signal.throwIfAborted(),
115
+ open: () => {
116
+ registerOpen(name, this[endpointSpec]);
117
+ },
118
+ close: () => this.#release(), stop: () => this.#release(),
119
+ send: request => this.send(request),
120
+ });
121
+ #release(): void {
122
+ releaseEndpoint(name, this[endpointSpec]);
123
+ }
124
+ }
125
+ class SyntheticAdapter extends Adapter<SyntheticEndpoint> {
126
+ protected createEndpoint(): SyntheticEndpoint { return new SyntheticEndpoint(); }
127
+ }
128
+ return [name, defineAdapter({ capabilities: ['inbound', 'outbound'],
129
+ segments: options.segments ?? { html: 'text', interactive: 'text', markdown: 'text' },
130
+ createAdapter: () => new SyntheticAdapter(),
131
+ })];
132
+ }));
104
133
  const modules = options.modules ?? new NativeDevelopmentModuleRuntime({ projectRoot, watch: false });
105
134
  this.#root = new RootRuntime({ projectRoot,
106
135
  modules: withTestEndpoints(modules, rootEntry, definitions),
@@ -24,7 +24,22 @@ export function withTestEndpoints(
24
24
  });
25
25
  return { ...loaded, default: definition } as T;
26
26
  },
27
- ...(delegate.loadClientModule ? { loadClientModule: delegate.loadClientModule.bind(delegate) } : {}),
27
+ loadClientModule: delegate.loadClientModule
28
+ ? delegate.loadClientModule.bind(delegate)
29
+ : async <T = unknown>(source: string): Promise<T> => {
30
+ let metadata: unknown;
31
+ try {
32
+ const loaded = await delegate.load<{ meta?: unknown }>(source);
33
+ metadata = loaded?.meta;
34
+ } catch {
35
+ // Browser-only client modules may not load in headless Node test mode; fall back to default metadata.
36
+ }
37
+ return Object.freeze({
38
+ module: source,
39
+ hash: 'test',
40
+ ...(metadata !== undefined ? { metadata } : {}),
41
+ }) as T;
42
+ },
28
43
  ...(delegate.invalidate ? { invalidate: delegate.invalidate.bind(delegate) } : {}),
29
44
  ...(delegate.affectedSources ? { affectedSources: delegate.affectedSources.bind(delegate) } : {}),
30
45
  ...(delegate.requiresProcessRestart ? { requiresProcessRestart: delegate.requiresProcessRestart.bind(delegate) } : {}),