opinionated-machine 10.3.0 → 10.4.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# opinionated-machine
|
|
2
2
|
|
|
3
|
+
## 10.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8a388e7: Expose the response body on `SSEInjectConnection`: `getBody()` returns the raw body string and `json<T>()` parses it as JSON, mirroring Fastify's inject response. This lets tests using the untyped `SSEInjectClient` assert on JSON error bodies that an SSE route sends before streaming starts (auth failures, validation errors, unavailable integrations), which previously were unreachable.
|
|
8
|
+
|
|
3
9
|
## 10.3.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1579,6 +1579,8 @@ it('streams chat completions', async () => {
|
|
|
1579
1579
|
})
|
|
1580
1580
|
```
|
|
1581
1581
|
|
|
1582
|
+
If the route answers with an error status before streaming starts, the response carries a JSON body instead of events - read it with `conn.getBody()` or `conn.json()` (see [SSEInjectClient](#sseinjectclient)).
|
|
1583
|
+
|
|
1582
1584
|
#### Asserting documented error responses with `bodyForStatus`
|
|
1583
1585
|
|
|
1584
1586
|
When a contract declares `responseBodySchemasByStatusCode` for non-2xx responses (the shape the handler emits via `sse.respond(status, body)` before streaming starts), `injectSSE` / `injectPayloadSSE` expose a typed `bodyForStatus(status)` accessor:
|
|
@@ -2463,6 +2465,25 @@ const events = conn.getReceivedEvents()
|
|
|
2463
2465
|
const chunks = events.filter(e => e.event === 'chunk')
|
|
2464
2466
|
```
|
|
2465
2467
|
|
|
2468
|
+
When the route answers with a status code *before* streaming starts (auth failure,
|
|
2469
|
+
validation error, integration unavailable), it sends a JSON body rather than events.
|
|
2470
|
+
`getBody()` returns that body raw and `json()` parses it, mirroring Fastify's own
|
|
2471
|
+
inject response:
|
|
2472
|
+
|
|
2473
|
+
```ts
|
|
2474
|
+
const conn = await client.connect('/api/export/progress')
|
|
2475
|
+
|
|
2476
|
+
expect(conn.getStatusCode()).toBe(503)
|
|
2477
|
+
expect(conn.json<{ errorCode: string }>()).toMatchObject({
|
|
2478
|
+
errorCode: 'INTEGRATION_NOT_AVAILABLE',
|
|
2479
|
+
})
|
|
2480
|
+
```
|
|
2481
|
+
|
|
2482
|
+
`json()` throws if the body is empty or isn't valid JSON, so it only makes sense for
|
|
2483
|
+
these pre-stream responses - a `text/event-stream` body is not JSON. For contract-typed
|
|
2484
|
+
tests, `injectSSE`/`injectPayloadSSE`/`injectApiSSE` offer `bodyForStatus(status)`, which
|
|
2485
|
+
also validates the body against the contract's schema for that status.
|
|
2486
|
+
|
|
2466
2487
|
#### Contract-Aware Inject Helpers
|
|
2467
2488
|
|
|
2468
2489
|
For typed testing with SSE contracts:
|
|
@@ -54,6 +54,23 @@ export declare class SSEInjectConnection implements SSETestConnection {
|
|
|
54
54
|
* Get the response headers.
|
|
55
55
|
*/
|
|
56
56
|
getHeaders(): Record<string, string | string[] | undefined>;
|
|
57
|
+
/**
|
|
58
|
+
* Get the raw response body as a string.
|
|
59
|
+
*
|
|
60
|
+
* For a streaming response this is the raw `text/event-stream` payload the
|
|
61
|
+
* events were parsed from. For a route that answered with a status code
|
|
62
|
+
* before streaming started (auth failure, validation error, integration
|
|
63
|
+
* unavailable) it is that non-streaming body - typically JSON.
|
|
64
|
+
*/
|
|
65
|
+
getBody(): string;
|
|
66
|
+
/**
|
|
67
|
+
* Parse the raw response body as JSON, mirroring Fastify's inject `json()`.
|
|
68
|
+
*
|
|
69
|
+
* @throws if the body is empty or not valid JSON. A `text/event-stream` body
|
|
70
|
+
* is not JSON, so this only makes sense for responses emitted before
|
|
71
|
+
* streaming started.
|
|
72
|
+
*/
|
|
73
|
+
json<T = unknown>(): T;
|
|
57
74
|
}
|
|
58
75
|
/**
|
|
59
76
|
* SSE client using Fastify's inject() for testing SSE endpoints.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseSSEEvents } from "../sse/sseParser.js";
|
|
2
|
+
import { truncateBody } from "./sseInjectShared.js";
|
|
2
3
|
/**
|
|
3
4
|
* SSE connection object returned by SSEInjectClient.
|
|
4
5
|
*
|
|
@@ -80,6 +81,36 @@ export class SSEInjectConnection {
|
|
|
80
81
|
getHeaders() {
|
|
81
82
|
return this.response.headers;
|
|
82
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Get the raw response body as a string.
|
|
86
|
+
*
|
|
87
|
+
* For a streaming response this is the raw `text/event-stream` payload the
|
|
88
|
+
* events were parsed from. For a route that answered with a status code
|
|
89
|
+
* before streaming started (auth failure, validation error, integration
|
|
90
|
+
* unavailable) it is that non-streaming body - typically JSON.
|
|
91
|
+
*/
|
|
92
|
+
getBody() {
|
|
93
|
+
return this.response.body;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse the raw response body as JSON, mirroring Fastify's inject `json()`.
|
|
97
|
+
*
|
|
98
|
+
* @throws if the body is empty or not valid JSON. A `text/event-stream` body
|
|
99
|
+
* is not JSON, so this only makes sense for responses emitted before
|
|
100
|
+
* streaming started.
|
|
101
|
+
*/
|
|
102
|
+
json() {
|
|
103
|
+
const { body } = this.response;
|
|
104
|
+
if (!body) {
|
|
105
|
+
throw new Error(`json() — response body is empty (status ${this.response.statusCode})`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(body);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
throw new Error(`json() — body is not valid JSON: ${err.message}; body: ${truncateBody(body)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
83
114
|
}
|
|
84
115
|
/**
|
|
85
116
|
* SSE client using Fastify's inject() for testing SSE endpoints.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sseInjectClient.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"sseInjectClient.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -60,6 +60,34 @@ export interface SSETestConnection {
|
|
|
60
60
|
* Get response headers.
|
|
61
61
|
*/
|
|
62
62
|
getHeaders(): Record<string, string | string[] | undefined>;
|
|
63
|
+
/**
|
|
64
|
+
* Get the raw response body as a string.
|
|
65
|
+
*
|
|
66
|
+
* For a successful SSE response this is the raw `text/event-stream` payload
|
|
67
|
+
* (already parsed into events, available via `getReceivedEvents()`). It is
|
|
68
|
+
* most useful when the route answered with a status code before streaming
|
|
69
|
+
* started - an auth failure, a validation error, an unavailable integration -
|
|
70
|
+
* and responded with a JSON body instead of events.
|
|
71
|
+
*/
|
|
72
|
+
getBody(): string;
|
|
73
|
+
/**
|
|
74
|
+
* Parse the raw response body as JSON.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors Fastify's own inject response `json()`. Intended for non-streaming
|
|
77
|
+
* responses emitted before streaming starts; calling it on an actual SSE
|
|
78
|
+
* stream body throws, since `text/event-stream` is not JSON.
|
|
79
|
+
*
|
|
80
|
+
* @throws if the body is empty or not valid JSON (the message includes a
|
|
81
|
+
* truncated body snippet).
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const conn = await client.connect('/api/stream')
|
|
86
|
+
* expect(conn.getStatusCode()).toBe(503)
|
|
87
|
+
* expect(conn.json()).toMatchObject({ errorCode: 'INTEGRATION_NOT_AVAILABLE' })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
json<T = unknown>(): T;
|
|
63
91
|
}
|
|
64
92
|
/**
|
|
65
93
|
* Options for establishing an SSE connection.
|