opinionated-machine 5.1.0 → 6.0.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.
Files changed (55) hide show
  1. package/README.md +966 -2
  2. package/dist/index.d.ts +3 -2
  3. package/dist/index.js +5 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/AbstractController.d.ts +3 -3
  6. package/dist/lib/AbstractController.js.map +1 -1
  7. package/dist/lib/AbstractModule.d.ts +23 -1
  8. package/dist/lib/AbstractModule.js +25 -0
  9. package/dist/lib/AbstractModule.js.map +1 -1
  10. package/dist/lib/DIContext.d.ts +35 -0
  11. package/dist/lib/DIContext.js +108 -1
  12. package/dist/lib/DIContext.js.map +1 -1
  13. package/dist/lib/resolverFunctions.d.ts +34 -0
  14. package/dist/lib/resolverFunctions.js +47 -0
  15. package/dist/lib/resolverFunctions.js.map +1 -1
  16. package/dist/lib/sse/AbstractSSEController.d.ts +163 -0
  17. package/dist/lib/sse/AbstractSSEController.js +228 -0
  18. package/dist/lib/sse/AbstractSSEController.js.map +1 -0
  19. package/dist/lib/sse/SSEConnectionSpy.d.ts +55 -0
  20. package/dist/lib/sse/SSEConnectionSpy.js +136 -0
  21. package/dist/lib/sse/SSEConnectionSpy.js.map +1 -0
  22. package/dist/lib/sse/index.d.ts +5 -0
  23. package/dist/lib/sse/index.js +6 -0
  24. package/dist/lib/sse/index.js.map +1 -0
  25. package/dist/lib/sse/sseContracts.d.ts +132 -0
  26. package/dist/lib/sse/sseContracts.js +102 -0
  27. package/dist/lib/sse/sseContracts.js.map +1 -0
  28. package/dist/lib/sse/sseParser.d.ts +167 -0
  29. package/dist/lib/sse/sseParser.js +225 -0
  30. package/dist/lib/sse/sseParser.js.map +1 -0
  31. package/dist/lib/sse/sseRouteBuilder.d.ts +47 -0
  32. package/dist/lib/sse/sseRouteBuilder.js +114 -0
  33. package/dist/lib/sse/sseRouteBuilder.js.map +1 -0
  34. package/dist/lib/sse/sseTypes.d.ts +164 -0
  35. package/dist/lib/sse/sseTypes.js +2 -0
  36. package/dist/lib/sse/sseTypes.js.map +1 -0
  37. package/dist/lib/testing/index.d.ts +5 -0
  38. package/dist/lib/testing/index.js +5 -0
  39. package/dist/lib/testing/index.js.map +1 -0
  40. package/dist/lib/testing/sseHttpClient.d.ts +203 -0
  41. package/dist/lib/testing/sseHttpClient.js +262 -0
  42. package/dist/lib/testing/sseHttpClient.js.map +1 -0
  43. package/dist/lib/testing/sseInjectClient.d.ts +173 -0
  44. package/dist/lib/testing/sseInjectClient.js +234 -0
  45. package/dist/lib/testing/sseInjectClient.js.map +1 -0
  46. package/dist/lib/testing/sseInjectHelpers.d.ts +59 -0
  47. package/dist/lib/testing/sseInjectHelpers.js +117 -0
  48. package/dist/lib/testing/sseInjectHelpers.js.map +1 -0
  49. package/dist/lib/testing/sseTestServer.d.ts +93 -0
  50. package/dist/lib/testing/sseTestServer.js +108 -0
  51. package/dist/lib/testing/sseTestServer.js.map +1 -0
  52. package/dist/lib/testing/sseTestTypes.d.ts +106 -0
  53. package/dist/lib/testing/sseTestTypes.js +2 -0
  54. package/dist/lib/testing/sseTestTypes.js.map +1 -0
  55. package/package.json +13 -11
@@ -0,0 +1,173 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { type ParsedSSEEvent } from '../sse/sseParser.ts';
3
+ import type { SSEConnectOptions, SSETestConnection } from './sseTestTypes.ts';
4
+ /**
5
+ * Response from a Fastify inject() call for SSE.
6
+ */
7
+ export type SSEInjectResponse = {
8
+ statusCode: number;
9
+ headers: Record<string, string | string[] | undefined>;
10
+ body: string;
11
+ };
12
+ /**
13
+ * SSE connection object returned by SSEInjectClient.
14
+ *
15
+ * Represents a completed SSE response from Fastify's inject().
16
+ * Since inject() waits for the complete response, all events
17
+ * are available immediately after construction.
18
+ */
19
+ export declare class SSEInjectConnection implements SSETestConnection {
20
+ private readonly receivedEvents;
21
+ private readonly response;
22
+ constructor(response: SSEInjectResponse);
23
+ /**
24
+ * Wait for a specific event by name.
25
+ * Since inject() returns the complete response, this searches
26
+ * the already-received events.
27
+ */
28
+ waitForEvent(eventName: string, timeout?: number): Promise<ParsedSSEEvent>;
29
+ /**
30
+ * Wait for a specific number of events.
31
+ * Since inject() returns the complete response, this checks
32
+ * the already-received events.
33
+ */
34
+ waitForEvents(count: number, timeout?: number): Promise<ParsedSSEEvent[]>;
35
+ /**
36
+ * Get all events received in the response.
37
+ */
38
+ getReceivedEvents(): ParsedSSEEvent[];
39
+ /**
40
+ * Close the connection. No-op for inject connections since
41
+ * the response is already complete.
42
+ */
43
+ close(): void;
44
+ /**
45
+ * Check if the connection has been closed.
46
+ * Always returns true for inject connections since response is complete.
47
+ */
48
+ isClosed(): boolean;
49
+ /**
50
+ * Get the HTTP status code from the response.
51
+ */
52
+ getStatusCode(): number;
53
+ /**
54
+ * Get the response headers.
55
+ */
56
+ getHeaders(): Record<string, string | string[] | undefined>;
57
+ }
58
+ /**
59
+ * SSE client using Fastify's inject() for testing SSE endpoints.
60
+ *
61
+ * This client uses Fastify's `inject()` method which simulates HTTP requests
62
+ * without network overhead. The key characteristic is that `inject()` waits
63
+ * for the **complete response** before returning, meaning:
64
+ *
65
+ * - All events are available immediately after connect() returns
66
+ * - The SSE handler must close the connection for connect() to complete
67
+ * - Best suited for SSE streams that have a defined end
68
+ *
69
+ * **Ideal for testing:**
70
+ * - OpenAI-style streaming (POST with body, streams tokens, then closes)
71
+ * - Short-lived streams that complete after sending all events
72
+ * - Endpoints where you want to test the full response at once
73
+ *
74
+ * **When to use SSEInjectClient vs SSEHttpClient:**
75
+ *
76
+ * | SSEInjectClient (this class) | SSEHttpClient |
77
+ * |-------------------------------------|--------------------------------------|
78
+ * | Fastify's inject() (no network) | Real HTTP connection via fetch() |
79
+ * | All events returned at once | Events arrive incrementally |
80
+ * | Handler must close the connection | Connection can stay open |
81
+ * | Works without starting server | Requires running server (listen()) |
82
+ * | Use for: OpenAI-style, completions | Use for: notifications, chat, feeds |
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * // Testing OpenAI-style chat completion streaming
87
+ * const client = new SSEInjectClient(app)
88
+ *
89
+ * // POST request that streams response and closes
90
+ * const conn = await client.connectWithBody(
91
+ * '/api/chat/completions',
92
+ * { model: 'gpt-4', messages: [{ role: 'user', content: 'Hello' }], stream: true }
93
+ * )
94
+ *
95
+ * // connect() returns after handler closes - all events are available
96
+ * expect(conn.getStatusCode()).toBe(200)
97
+ *
98
+ * // Get all events that were streamed
99
+ * const events = conn.getReceivedEvents()
100
+ * expect(events[events.length - 1].event).toBe('done')
101
+ *
102
+ * // Parse the streamed content
103
+ * const chunks = events
104
+ * .filter(e => e.event === 'chunk')
105
+ * .map(e => JSON.parse(e.data).content)
106
+ * const fullResponse = chunks.join('')
107
+ * ```
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * // Testing GET SSE endpoint
112
+ * const client = new SSEInjectClient(app)
113
+ * const conn = await client.connect('/api/export/progress', {
114
+ * headers: { authorization: 'Bearer token' }
115
+ * })
116
+ *
117
+ * // Wait for specific event type
118
+ * const completeEvent = await conn.waitForEvent('complete')
119
+ * expect(JSON.parse(completeEvent.data)).toMatchObject({ status: 'success' })
120
+ * ```
121
+ */
122
+ export declare class SSEInjectClient {
123
+ private readonly app;
124
+ /**
125
+ * Create a new SSE inject client.
126
+ * @param app - Fastify instance (does not need to be listening)
127
+ */
128
+ constructor(app: FastifyInstance<any, any, any, any>);
129
+ /**
130
+ * Send a GET request to an SSE endpoint.
131
+ *
132
+ * Returns when the SSE handler closes the connection.
133
+ * All events are then available via getReceivedEvents().
134
+ *
135
+ * @param url - The endpoint URL (e.g., '/api/stream')
136
+ * @param options - Optional headers
137
+ * @returns Connection object with all received events
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const conn = await client.connect('/api/notifications/stream', {
142
+ * headers: { authorization: 'Bearer token' }
143
+ * })
144
+ * const events = conn.getReceivedEvents()
145
+ * ```
146
+ */
147
+ connect(url: string, options?: Omit<SSEConnectOptions, 'method' | 'body'>): Promise<SSEInjectConnection>;
148
+ /**
149
+ * Send a POST/PUT/PATCH request to an SSE endpoint with a body.
150
+ *
151
+ * This is the typical pattern for OpenAI-style streaming APIs where
152
+ * you send a request body and receive a streamed response.
153
+ *
154
+ * Returns when the SSE handler closes the connection.
155
+ * All events are then available via getReceivedEvents().
156
+ *
157
+ * @param url - The endpoint URL (e.g., '/api/chat/completions')
158
+ * @param body - Request body (will be JSON stringified)
159
+ * @param options - Optional method (defaults to POST) and headers
160
+ * @returns Connection object with all received events
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * const conn = await client.connectWithBody(
165
+ * '/api/chat/completions',
166
+ * { model: 'gpt-4', messages: [...], stream: true },
167
+ * { headers: { authorization: 'Bearer sk-...' } }
168
+ * )
169
+ * const chunks = conn.getReceivedEvents().filter(e => e.event === 'chunk')
170
+ * ```
171
+ */
172
+ connectWithBody(url: string, body: unknown, options?: Omit<SSEConnectOptions, 'body'>): Promise<SSEInjectConnection>;
173
+ }
@@ -0,0 +1,234 @@
1
+ import { parseSSEEvents } from "../sse/sseParser.js";
2
+ /**
3
+ * SSE connection object returned by SSEInjectClient.
4
+ *
5
+ * Represents a completed SSE response from Fastify's inject().
6
+ * Since inject() waits for the complete response, all events
7
+ * are available immediately after construction.
8
+ */
9
+ export class SSEInjectConnection {
10
+ receivedEvents = [];
11
+ response;
12
+ constructor(response) {
13
+ this.response = response;
14
+ // Parse all events from response body (inject waits for complete response)
15
+ if (response.body) {
16
+ const events = parseSSEEvents(response.body);
17
+ this.receivedEvents.push(...events);
18
+ }
19
+ }
20
+ /**
21
+ * Wait for a specific event by name.
22
+ * Since inject() returns the complete response, this searches
23
+ * the already-received events.
24
+ */
25
+ async waitForEvent(eventName, timeout = 5000) {
26
+ const startTime = Date.now();
27
+ while (Date.now() - startTime < timeout) {
28
+ const event = this.receivedEvents.find((e) => e.event === eventName);
29
+ if (event) {
30
+ return event;
31
+ }
32
+ await new Promise((resolve) => setTimeout(resolve, 10));
33
+ }
34
+ throw new Error(`Timeout waiting for event: ${eventName}`);
35
+ }
36
+ /**
37
+ * Wait for a specific number of events.
38
+ * Since inject() returns the complete response, this checks
39
+ * the already-received events.
40
+ */
41
+ async waitForEvents(count, timeout = 5000) {
42
+ const startTime = Date.now();
43
+ while (Date.now() - startTime < timeout) {
44
+ if (this.receivedEvents.length >= count) {
45
+ return this.receivedEvents.slice(0, count);
46
+ }
47
+ await new Promise((resolve) => setTimeout(resolve, 10));
48
+ }
49
+ throw new Error(`Timeout waiting for ${count} events, received ${this.receivedEvents.length}`);
50
+ }
51
+ /**
52
+ * Get all events received in the response.
53
+ */
54
+ getReceivedEvents() {
55
+ return [...this.receivedEvents];
56
+ }
57
+ /**
58
+ * Close the connection. No-op for inject connections since
59
+ * the response is already complete.
60
+ */
61
+ close() {
62
+ // No-op - inject() responses are already complete
63
+ }
64
+ /**
65
+ * Check if the connection has been closed.
66
+ * Always returns true for inject connections since response is complete.
67
+ */
68
+ isClosed() {
69
+ return true;
70
+ }
71
+ /**
72
+ * Get the HTTP status code from the response.
73
+ */
74
+ getStatusCode() {
75
+ return this.response.statusCode;
76
+ }
77
+ /**
78
+ * Get the response headers.
79
+ */
80
+ getHeaders() {
81
+ return this.response.headers;
82
+ }
83
+ }
84
+ /**
85
+ * SSE client using Fastify's inject() for testing SSE endpoints.
86
+ *
87
+ * This client uses Fastify's `inject()` method which simulates HTTP requests
88
+ * without network overhead. The key characteristic is that `inject()` waits
89
+ * for the **complete response** before returning, meaning:
90
+ *
91
+ * - All events are available immediately after connect() returns
92
+ * - The SSE handler must close the connection for connect() to complete
93
+ * - Best suited for SSE streams that have a defined end
94
+ *
95
+ * **Ideal for testing:**
96
+ * - OpenAI-style streaming (POST with body, streams tokens, then closes)
97
+ * - Short-lived streams that complete after sending all events
98
+ * - Endpoints where you want to test the full response at once
99
+ *
100
+ * **When to use SSEInjectClient vs SSEHttpClient:**
101
+ *
102
+ * | SSEInjectClient (this class) | SSEHttpClient |
103
+ * |-------------------------------------|--------------------------------------|
104
+ * | Fastify's inject() (no network) | Real HTTP connection via fetch() |
105
+ * | All events returned at once | Events arrive incrementally |
106
+ * | Handler must close the connection | Connection can stay open |
107
+ * | Works without starting server | Requires running server (listen()) |
108
+ * | Use for: OpenAI-style, completions | Use for: notifications, chat, feeds |
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * // Testing OpenAI-style chat completion streaming
113
+ * const client = new SSEInjectClient(app)
114
+ *
115
+ * // POST request that streams response and closes
116
+ * const conn = await client.connectWithBody(
117
+ * '/api/chat/completions',
118
+ * { model: 'gpt-4', messages: [{ role: 'user', content: 'Hello' }], stream: true }
119
+ * )
120
+ *
121
+ * // connect() returns after handler closes - all events are available
122
+ * expect(conn.getStatusCode()).toBe(200)
123
+ *
124
+ * // Get all events that were streamed
125
+ * const events = conn.getReceivedEvents()
126
+ * expect(events[events.length - 1].event).toBe('done')
127
+ *
128
+ * // Parse the streamed content
129
+ * const chunks = events
130
+ * .filter(e => e.event === 'chunk')
131
+ * .map(e => JSON.parse(e.data).content)
132
+ * const fullResponse = chunks.join('')
133
+ * ```
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * // Testing GET SSE endpoint
138
+ * const client = new SSEInjectClient(app)
139
+ * const conn = await client.connect('/api/export/progress', {
140
+ * headers: { authorization: 'Bearer token' }
141
+ * })
142
+ *
143
+ * // Wait for specific event type
144
+ * const completeEvent = await conn.waitForEvent('complete')
145
+ * expect(JSON.parse(completeEvent.data)).toMatchObject({ status: 'success' })
146
+ * ```
147
+ */
148
+ export class SSEInjectClient {
149
+ // biome-ignore lint/suspicious/noExplicitAny: Fastify instance types are complex
150
+ app;
151
+ /**
152
+ * Create a new SSE inject client.
153
+ * @param app - Fastify instance (does not need to be listening)
154
+ */
155
+ // biome-ignore lint/suspicious/noExplicitAny: Fastify instance types are complex
156
+ constructor(app) {
157
+ this.app = app;
158
+ }
159
+ /**
160
+ * Send a GET request to an SSE endpoint.
161
+ *
162
+ * Returns when the SSE handler closes the connection.
163
+ * All events are then available via getReceivedEvents().
164
+ *
165
+ * @param url - The endpoint URL (e.g., '/api/stream')
166
+ * @param options - Optional headers
167
+ * @returns Connection object with all received events
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * const conn = await client.connect('/api/notifications/stream', {
172
+ * headers: { authorization: 'Bearer token' }
173
+ * })
174
+ * const events = conn.getReceivedEvents()
175
+ * ```
176
+ */
177
+ async connect(url, options) {
178
+ const response = await this.app.inject({
179
+ method: 'GET',
180
+ url,
181
+ headers: {
182
+ accept: 'text/event-stream',
183
+ ...options?.headers,
184
+ },
185
+ });
186
+ return new SSEInjectConnection({
187
+ statusCode: response.statusCode,
188
+ headers: response.headers,
189
+ body: response.body,
190
+ });
191
+ }
192
+ /**
193
+ * Send a POST/PUT/PATCH request to an SSE endpoint with a body.
194
+ *
195
+ * This is the typical pattern for OpenAI-style streaming APIs where
196
+ * you send a request body and receive a streamed response.
197
+ *
198
+ * Returns when the SSE handler closes the connection.
199
+ * All events are then available via getReceivedEvents().
200
+ *
201
+ * @param url - The endpoint URL (e.g., '/api/chat/completions')
202
+ * @param body - Request body (will be JSON stringified)
203
+ * @param options - Optional method (defaults to POST) and headers
204
+ * @returns Connection object with all received events
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const conn = await client.connectWithBody(
209
+ * '/api/chat/completions',
210
+ * { model: 'gpt-4', messages: [...], stream: true },
211
+ * { headers: { authorization: 'Bearer sk-...' } }
212
+ * )
213
+ * const chunks = conn.getReceivedEvents().filter(e => e.event === 'chunk')
214
+ * ```
215
+ */
216
+ async connectWithBody(url, body, options) {
217
+ const response = await this.app.inject({
218
+ method: options?.method ?? 'POST',
219
+ url,
220
+ headers: {
221
+ accept: 'text/event-stream',
222
+ 'content-type': 'application/json',
223
+ ...options?.headers,
224
+ },
225
+ payload: JSON.stringify(body),
226
+ });
227
+ return new SSEInjectConnection({
228
+ statusCode: response.statusCode,
229
+ headers: response.headers,
230
+ body: response.body,
231
+ });
232
+ }
233
+ }
234
+ //# sourceMappingURL=sseInjectClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sseInjectClient.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { z } from 'zod';
3
+ import type { SSERouteDefinition } from '../sse/sseContracts.ts';
4
+ import type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult } from './sseTestTypes.ts';
5
+ /**
6
+ * Build URL from contract path and params.
7
+ * @internal
8
+ */
9
+ export declare function buildUrl<Contract extends {
10
+ path: string;
11
+ }>(contract: Contract, params?: Record<string, string>, query?: Record<string, unknown>): string;
12
+ /**
13
+ * Inject a GET SSE request using a contract definition.
14
+ *
15
+ * Best for testing SSE endpoints that complete (streaming responses).
16
+ * For long-lived connections, use `connectSSE` with a real HTTP server.
17
+ *
18
+ * @param app - Fastify instance
19
+ * @param contract - SSE route contract
20
+ * @param options - Request options (params, query, headers)
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const { closed } = injectSSE(app, streamContract, {
25
+ * query: { userId: 'user-123' },
26
+ * })
27
+ * const result = await closed
28
+ * const events = parseSSEEvents(result.body)
29
+ * ```
30
+ */
31
+ export declare function injectSSE<Contract extends SSERouteDefinition<'GET', string, z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, undefined, Record<string, z.ZodTypeAny>>>(app: FastifyInstance<any, any, any, any>, contract: Contract, options?: InjectSSEOptions<Contract>): InjectSSEResult;
32
+ /**
33
+ * Inject a POST/PUT/PATCH SSE request using a contract definition.
34
+ *
35
+ * This helper is designed for testing OpenAI-style streaming APIs where
36
+ * the request includes a body and the response streams events.
37
+ *
38
+ * @param app - Fastify instance
39
+ * @param contract - SSE route contract with body
40
+ * @param options - Request options (params, query, headers, body)
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * // Fire the SSE request
45
+ * const { closed } = injectPayloadSSE(app, chatCompletionContract, {
46
+ * body: { message: 'Hello', stream: true },
47
+ * headers: { authorization: 'Bearer token' },
48
+ * })
49
+ *
50
+ * // Wait for streaming to complete and get full response
51
+ * const result = await closed
52
+ * const events = parseSSEEvents(result.body)
53
+ *
54
+ * expect(events).toContainEqual(
55
+ * expect.objectContaining({ event: 'chunk' })
56
+ * )
57
+ * ```
58
+ */
59
+ export declare function injectPayloadSSE<Contract extends SSERouteDefinition<'POST' | 'PUT' | 'PATCH', string, z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, Record<string, z.ZodTypeAny>>>(app: FastifyInstance<any, any, any, any>, contract: Contract, options: InjectPayloadSSEOptions<Contract>): InjectSSEResult;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Build URL from contract path and params.
3
+ * @internal
4
+ */
5
+ export function buildUrl(contract, params, query) {
6
+ let url = contract.path;
7
+ // Substitute path params
8
+ if (params) {
9
+ for (const [key, value] of Object.entries(params)) {
10
+ url = url.replace(`:${key}`, encodeURIComponent(String(value)));
11
+ }
12
+ }
13
+ // Add query string
14
+ if (query && Object.keys(query).length > 0) {
15
+ const searchParams = new URLSearchParams();
16
+ for (const [key, value] of Object.entries(query)) {
17
+ if (value !== undefined && value !== null) {
18
+ searchParams.append(key, String(value));
19
+ }
20
+ }
21
+ const queryString = searchParams.toString();
22
+ if (queryString) {
23
+ url = `${url}?${queryString}`;
24
+ }
25
+ }
26
+ return url;
27
+ }
28
+ /**
29
+ * Inject a GET SSE request using a contract definition.
30
+ *
31
+ * Best for testing SSE endpoints that complete (streaming responses).
32
+ * For long-lived connections, use `connectSSE` with a real HTTP server.
33
+ *
34
+ * @param app - Fastify instance
35
+ * @param contract - SSE route contract
36
+ * @param options - Request options (params, query, headers)
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const { closed } = injectSSE(app, streamContract, {
41
+ * query: { userId: 'user-123' },
42
+ * })
43
+ * const result = await closed
44
+ * const events = parseSSEEvents(result.body)
45
+ * ```
46
+ */
47
+ export function injectSSE(
48
+ // biome-ignore lint/suspicious/noExplicitAny: Fastify instance types are complex
49
+ app, contract, options) {
50
+ const url = buildUrl(contract, options?.params, options?.query);
51
+ // Start the request - this promise resolves when connection closes
52
+ const closed = app
53
+ .inject({
54
+ method: 'GET',
55
+ url,
56
+ headers: {
57
+ accept: 'text/event-stream',
58
+ ...options?.headers,
59
+ },
60
+ })
61
+ .then((res) => ({
62
+ statusCode: res.statusCode,
63
+ headers: res.headers,
64
+ body: res.body,
65
+ }));
66
+ return { closed };
67
+ }
68
+ /**
69
+ * Inject a POST/PUT/PATCH SSE request using a contract definition.
70
+ *
71
+ * This helper is designed for testing OpenAI-style streaming APIs where
72
+ * the request includes a body and the response streams events.
73
+ *
74
+ * @param app - Fastify instance
75
+ * @param contract - SSE route contract with body
76
+ * @param options - Request options (params, query, headers, body)
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * // Fire the SSE request
81
+ * const { closed } = injectPayloadSSE(app, chatCompletionContract, {
82
+ * body: { message: 'Hello', stream: true },
83
+ * headers: { authorization: 'Bearer token' },
84
+ * })
85
+ *
86
+ * // Wait for streaming to complete and get full response
87
+ * const result = await closed
88
+ * const events = parseSSEEvents(result.body)
89
+ *
90
+ * expect(events).toContainEqual(
91
+ * expect.objectContaining({ event: 'chunk' })
92
+ * )
93
+ * ```
94
+ */
95
+ export function injectPayloadSSE(
96
+ // biome-ignore lint/suspicious/noExplicitAny: Fastify instance types are complex
97
+ app, contract, options) {
98
+ const url = buildUrl(contract, options.params, options.query);
99
+ const closed = app
100
+ .inject({
101
+ method: contract.method,
102
+ url,
103
+ headers: {
104
+ accept: 'text/event-stream',
105
+ 'content-type': 'application/json',
106
+ ...options.headers,
107
+ },
108
+ payload: JSON.stringify(options.body),
109
+ })
110
+ .then((res) => ({
111
+ statusCode: res.statusCode,
112
+ headers: res.headers,
113
+ body: res.body,
114
+ }));
115
+ return { closed };
116
+ }
117
+ //# sourceMappingURL=sseInjectHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sseInjectHelpers.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectHelpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ import { type FastifyInstance } from 'fastify';
2
+ import type { CreateSSETestServerOptions } from './sseTestTypes.ts';
3
+ /**
4
+ * Test server for SSE e2e testing with automatic setup and cleanup.
5
+ *
6
+ * This class simplifies SSE e2e test setup by:
7
+ * - Creating a Fastify instance with @fastify/sse plugin pre-registered
8
+ * - Starting a real HTTP server on a random port
9
+ * - Providing a base URL for making HTTP requests
10
+ * - Handling cleanup on close()
11
+ *
12
+ * **When to use SSETestServer:**
13
+ * - Testing with `SSEHttpClient` (requires real HTTP server)
14
+ * - E2E tests that need to verify actual network behavior
15
+ * - Tests that need to run controller code in a real server context
16
+ *
17
+ * **Note:** For simple tests using `SSEInjectClient`, you don't need this class -
18
+ * you can use the Fastify instance directly without starting a server.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * // Basic usage
23
+ * const server = await SSETestServer.create(async (app) => {
24
+ * // Register your SSE routes
25
+ * app.get('/api/events', async (request, reply) => {
26
+ * reply.sse({ event: 'message', data: { hello: 'world' } })
27
+ * reply.sseClose()
28
+ * })
29
+ * })
30
+ *
31
+ * // Connect using SSEHttpClient
32
+ * const client = await SSEHttpClient.connect(server.baseUrl, '/api/events')
33
+ * const events = await client.collectEvents(1)
34
+ * expect(events[0].event).toBe('message')
35
+ *
36
+ * // Cleanup
37
+ * client.close()
38
+ * await server.close()
39
+ * ```
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // With custom resources (e.g., DI container, controllers)
44
+ * const server = await SSETestServer.create(
45
+ * async (app) => {
46
+ * // Routes can access resources via closure
47
+ * myController.registerRoutes(app)
48
+ * },
49
+ * {
50
+ * configureApp: async (app) => {
51
+ * // Configure validators, plugins, etc.
52
+ * app.setValidatorCompiler(validatorCompiler)
53
+ * },
54
+ * setup: async () => {
55
+ * // Create resources that will be available via server.resources
56
+ * const container = createContainer()
57
+ * const controller = container.resolve('sseController')
58
+ * return { container, controller }
59
+ * },
60
+ * }
61
+ * )
62
+ *
63
+ * // Access resources to interact with the server
64
+ * const { controller } = server.resources
65
+ * controller.broadcastEvent({ event: 'update', data: { value: 42 } })
66
+ *
67
+ * await server.close()
68
+ * ```
69
+ */
70
+ export declare class SSETestServer<T = undefined> {
71
+ /** The Fastify instance */
72
+ readonly app: FastifyInstance;
73
+ /** Base URL for the running server (e.g., "http://localhost:3000") */
74
+ readonly baseUrl: string;
75
+ /** Custom resources from setup function */
76
+ readonly resources: T;
77
+ private constructor();
78
+ /**
79
+ * Create and start a test server.
80
+ * @param registerRoutes - Function to register routes on the Fastify instance
81
+ */
82
+ static create(registerRoutes: (app: FastifyInstance) => void | Promise<void>): Promise<SSETestServer<undefined>>;
83
+ /**
84
+ * Create and start a test server with custom options and resources.
85
+ * @param registerRoutes - Function to register routes on the Fastify instance
86
+ * @param options - Configuration options including setup function
87
+ */
88
+ static create<T>(registerRoutes: (app: FastifyInstance) => void | Promise<void>, options: CreateSSETestServerOptions<T>): Promise<SSETestServer<T>>;
89
+ /**
90
+ * Close the server and cleanup resources.
91
+ */
92
+ close(): Promise<void>;
93
+ }