opinionated-machine 5.1.0 → 5.2.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/README.md +1237 -278
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/AbstractController.d.ts +3 -3
- package/dist/lib/AbstractController.js.map +1 -1
- package/dist/lib/AbstractModule.d.ts +14 -0
- package/dist/lib/AbstractModule.js +16 -0
- package/dist/lib/AbstractModule.js.map +1 -1
- package/dist/lib/DIContext.d.ts +35 -0
- package/dist/lib/DIContext.js +99 -0
- package/dist/lib/DIContext.js.map +1 -1
- package/dist/lib/resolverFunctions.d.ts +33 -0
- package/dist/lib/resolverFunctions.js +46 -0
- package/dist/lib/resolverFunctions.js.map +1 -1
- package/dist/lib/sse/AbstractSSEController.d.ts +163 -0
- package/dist/lib/sse/AbstractSSEController.js +228 -0
- package/dist/lib/sse/AbstractSSEController.js.map +1 -0
- package/dist/lib/sse/SSEConnectionSpy.d.ts +55 -0
- package/dist/lib/sse/SSEConnectionSpy.js +136 -0
- package/dist/lib/sse/SSEConnectionSpy.js.map +1 -0
- package/dist/lib/sse/index.d.ts +5 -0
- package/dist/lib/sse/index.js +6 -0
- package/dist/lib/sse/index.js.map +1 -0
- package/dist/lib/sse/sseContracts.d.ts +132 -0
- package/dist/lib/sse/sseContracts.js +102 -0
- package/dist/lib/sse/sseContracts.js.map +1 -0
- package/dist/lib/sse/sseParser.d.ts +167 -0
- package/dist/lib/sse/sseParser.js +225 -0
- package/dist/lib/sse/sseParser.js.map +1 -0
- package/dist/lib/sse/sseRouteBuilder.d.ts +47 -0
- package/dist/lib/sse/sseRouteBuilder.js +114 -0
- package/dist/lib/sse/sseRouteBuilder.js.map +1 -0
- package/dist/lib/sse/sseTypes.d.ts +164 -0
- package/dist/lib/sse/sseTypes.js +2 -0
- package/dist/lib/sse/sseTypes.js.map +1 -0
- package/dist/lib/testing/index.d.ts +5 -0
- package/dist/lib/testing/index.js +5 -0
- package/dist/lib/testing/index.js.map +1 -0
- package/dist/lib/testing/sseHttpClient.d.ts +203 -0
- package/dist/lib/testing/sseHttpClient.js +262 -0
- package/dist/lib/testing/sseHttpClient.js.map +1 -0
- package/dist/lib/testing/sseInjectClient.d.ts +173 -0
- package/dist/lib/testing/sseInjectClient.js +234 -0
- package/dist/lib/testing/sseInjectClient.js.map +1 -0
- package/dist/lib/testing/sseInjectHelpers.d.ts +59 -0
- package/dist/lib/testing/sseInjectHelpers.js +117 -0
- package/dist/lib/testing/sseInjectHelpers.js.map +1 -0
- package/dist/lib/testing/sseTestServer.d.ts +93 -0
- package/dist/lib/testing/sseTestServer.js +108 -0
- package/dist/lib/testing/sseTestServer.js.map +1 -0
- package/dist/lib/testing/sseTestTypes.d.ts +106 -0
- package/dist/lib/testing/sseTestTypes.js +2 -0
- package/dist/lib/testing/sseTestTypes.js.map +1 -0
- package/package.json +80 -78
|
@@ -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
|
+
}
|