@chidchanun/bcp 0.2.13 → 0.2.15
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 +224 -99
- package/docs/README.md +40 -35
- package/docs/api-manifest.json +26 -10
- package/docs/api-reference.md +173 -29
- package/docs/docs-web-manifest.json +8 -4
- package/docs/platform-manifest.json +33 -4
- package/docs/plugin-module-platform.md +391 -0
- package/docs/releases/0.2.14.md +241 -0
- package/docs/releases/0.2.15.md +118 -0
- package/docs/testing-platform.md +602 -0
- package/package.json +11 -1
- package/packages/bundler/src/client-boundary.ts +2 -0
- package/packages/client/src/plugins.mjs +811 -0
- package/packages/client/src/plugins.ts +26 -0
- package/packages/client/src/realtime.mjs +67 -30
- package/packages/client/src/testing.mjs +2357 -0
- package/packages/client/src/testing.ts +52 -0
- package/packages/server/src/plugins.ts +1225 -0
- package/packages/server/src/realtime.ts +95 -41
- package/packages/server/src/testing-page.ts +274 -0
- package/packages/server/src/testing.ts +1884 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# BCP Framework 0.2.15 — Plugin & Module Platform
|
|
2
|
+
|
|
3
|
+
**Release state:** unreleased
|
|
4
|
+
|
|
5
|
+
BCP `0.2.15` adds a server-only plugin/module composition layer through `bcp/plugins`.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- new `bcp/plugins` public entrypoint,
|
|
10
|
+
- `definePlugin()` plugin definitions,
|
|
11
|
+
- `defineModule()` named plugin bundles,
|
|
12
|
+
- `createPluginHost()` lifecycle orchestration,
|
|
13
|
+
- required and optional plugin dependencies,
|
|
14
|
+
- deterministic topological dependency ordering,
|
|
15
|
+
- dependency cycle and missing-dependency errors,
|
|
16
|
+
- setup/start/stop/dispose lifecycle hooks,
|
|
17
|
+
- reverse shutdown/disposal ordering,
|
|
18
|
+
- startup rollback when a dependent plugin fails,
|
|
19
|
+
- inspectable plugin runtime state,
|
|
20
|
+
- plugin-local config parser/schema contract,
|
|
21
|
+
- host-level config overrides,
|
|
22
|
+
- shared string/Symbol service registry,
|
|
23
|
+
- asynchronous in-process hook bus,
|
|
24
|
+
- server-only browser boundary,
|
|
25
|
+
- compiled `plugins.mjs` npm runtime,
|
|
26
|
+
- unit and prepared-package smoke coverage.
|
|
27
|
+
|
|
28
|
+
## Public API
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
createPluginHookBus,
|
|
33
|
+
createPluginHost,
|
|
34
|
+
createPluginServiceRegistry,
|
|
35
|
+
defineModule,
|
|
36
|
+
definePlugin,
|
|
37
|
+
PluginDependencyError,
|
|
38
|
+
PluginLifecycleError,
|
|
39
|
+
} from "bcp/plugins";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Dependency lifecycle
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
database
|
|
46
|
+
↓
|
|
47
|
+
jobs
|
|
48
|
+
↓
|
|
49
|
+
workflow
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Startup follows dependency order. Stop/dispose runs in reverse order.
|
|
53
|
+
|
|
54
|
+
If a plugin fails during startup, already-started plugins from that transition are stopped before the failure is propagated.
|
|
55
|
+
|
|
56
|
+
## Modules
|
|
57
|
+
|
|
58
|
+
Modules are composition bundles rather than a second runtime:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const backend =
|
|
62
|
+
defineModule({
|
|
63
|
+
name: "backend",
|
|
64
|
+
plugins: [
|
|
65
|
+
databasePlugin,
|
|
66
|
+
jobsPlugin,
|
|
67
|
+
workflowPlugin,
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
All plugins still share one host-level dependency graph.
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
Plugin definitions can provide defaults and a parser/schema. `PluginHostOptions.configs` can override raw configuration per plugin before parsing.
|
|
77
|
+
|
|
78
|
+
Invalid configuration fails at the plugin setup boundary.
|
|
79
|
+
|
|
80
|
+
## Extension services
|
|
81
|
+
|
|
82
|
+
`PluginServiceRegistry` provides explicit shared service lookup between plugins.
|
|
83
|
+
|
|
84
|
+
`PluginHookBus` provides awaited in-process extension hooks. It is not a durable event system; `bcp/events` remains the durable integration-event platform.
|
|
85
|
+
|
|
86
|
+
## Package/runtime contract
|
|
87
|
+
|
|
88
|
+
The prepared npm package exposes:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
bcp/plugins
|
|
92
|
+
types -> packages/client/src/plugins.ts
|
|
93
|
+
browser -> packages/client/src/server-only.browser.mjs
|
|
94
|
+
default -> packages/client/src/plugins.mjs
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`plugins.mjs` is compiled during package preparation.
|
|
98
|
+
|
|
99
|
+
## Compatibility
|
|
100
|
+
|
|
101
|
+
`0.2.15` has no intentional breaking changes from `0.2.14`.
|
|
102
|
+
|
|
103
|
+
Existing BCP entrypoints remain supported.
|
|
104
|
+
|
|
105
|
+
## Validation
|
|
106
|
+
|
|
107
|
+
Before publishing:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm run typecheck
|
|
111
|
+
npm run test:unit
|
|
112
|
+
npm run test:integration
|
|
113
|
+
npm run test:e2e
|
|
114
|
+
npm run test:package
|
|
115
|
+
npm run rc:check
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Do not tag or publish until the exact final release commit passes the full RC sequence.
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
# Testing Platform
|
|
2
|
+
|
|
3
|
+
BCP `0.2.14` adds the server-only `bcp/testing` entrypoint for framework-native testing without forcing Jest, Vitest or another test runner.
|
|
4
|
+
|
|
5
|
+
The APIs work with Node's built-in `node:test`, but they are assertion-runner neutral and can also be used from other test runners.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
createRouteTestHandler,
|
|
12
|
+
createTestApp,
|
|
13
|
+
expectResponse,
|
|
14
|
+
runTestPageAction,
|
|
15
|
+
runTestPageGuards,
|
|
16
|
+
runTestPageLoader,
|
|
17
|
+
} from "bcp/testing";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`bcp/testing` is server-only and must not be imported from browser/client bundles.
|
|
21
|
+
|
|
22
|
+
## Request harness
|
|
23
|
+
|
|
24
|
+
`createTestApp()` wraps a Request -> Response handler and provides request helpers, default headers and an in-memory cookie jar.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const app =
|
|
28
|
+
createTestApp({
|
|
29
|
+
handler:
|
|
30
|
+
async request => {
|
|
31
|
+
return Response.json({
|
|
32
|
+
method:
|
|
33
|
+
request.method,
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const response =
|
|
39
|
+
await app.get("/api/health");
|
|
40
|
+
|
|
41
|
+
await expectResponse(response)
|
|
42
|
+
.status(200)
|
|
43
|
+
.json({
|
|
44
|
+
method: "GET",
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Available request methods:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
request()
|
|
52
|
+
get()
|
|
53
|
+
post()
|
|
54
|
+
put()
|
|
55
|
+
patch()
|
|
56
|
+
delete()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
JSON request bodies can be sent with:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
await app.post(
|
|
63
|
+
"/api/users",
|
|
64
|
+
{
|
|
65
|
+
json: {
|
|
66
|
+
name: "BCP",
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
When a response contains `Set-Cookie`, the test app updates its cookie jar and sends those cookies on later requests.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
app.setCookie(
|
|
76
|
+
"theme",
|
|
77
|
+
"dark"
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
app.cookies();
|
|
81
|
+
app.clearCookie("theme");
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Default headers can also be changed between requests with `setHeader()` / `deleteHeader()`. Call `app.reset()` to restore the initial headers and cookies.
|
|
85
|
+
|
|
86
|
+
## API route handler testing
|
|
87
|
+
|
|
88
|
+
`createRouteTestHandler()` turns a BCP-style route module into a Request -> Response handler.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const handler =
|
|
92
|
+
createRouteTestHandler({
|
|
93
|
+
GET() {
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async POST(request) {
|
|
100
|
+
return Response.json({
|
|
101
|
+
received:
|
|
102
|
+
await request.json(),
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Supported route method exports:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
GET
|
|
112
|
+
POST
|
|
113
|
+
PUT
|
|
114
|
+
PATCH
|
|
115
|
+
DELETE
|
|
116
|
+
HEAD
|
|
117
|
+
OPTIONS
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Missing methods return `405 Method Not Allowed` with an `Allow` header.
|
|
121
|
+
|
|
122
|
+
Non-Response route values are normalized for tests:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
object/array/value -> Response.json(value)
|
|
126
|
+
string -> text Response
|
|
127
|
+
undefined/null -> 204 Response
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A custom route context can be supplied through `createRouteTestHandler(route, { context })`.
|
|
131
|
+
|
|
132
|
+
## Page guard testing
|
|
133
|
+
|
|
134
|
+
`runTestPageGuards()` calls the real `executePageGuardFunctions()` runtime.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const result =
|
|
138
|
+
await runTestPageGuards(
|
|
139
|
+
[
|
|
140
|
+
({ params }) => ({
|
|
141
|
+
userId:
|
|
142
|
+
params.userId,
|
|
143
|
+
}),
|
|
144
|
+
({ guardData }) => ({
|
|
145
|
+
allowed:
|
|
146
|
+
guardData.userId ===
|
|
147
|
+
"42",
|
|
148
|
+
}),
|
|
149
|
+
],
|
|
150
|
+
{
|
|
151
|
+
params: {
|
|
152
|
+
userId: "42",
|
|
153
|
+
},
|
|
154
|
+
url:
|
|
155
|
+
"/users/42?mode=edit",
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Guard data is chained in the same order as production execution. A guard `Response` such as a redirect stops the chain and is returned in `result.response`.
|
|
161
|
+
|
|
162
|
+
The helper accepts either guard functions directly or `PageGuardDefinition` objects when a specific label/file path is useful.
|
|
163
|
+
|
|
164
|
+
## Page loader testing
|
|
165
|
+
|
|
166
|
+
`runTestPageLoader()` calls the production `executePageLoaderFunction()` path and provides params, search params and guard data.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const result =
|
|
170
|
+
await runTestPageLoader(
|
|
171
|
+
async ({
|
|
172
|
+
params,
|
|
173
|
+
searchParams,
|
|
174
|
+
guardData,
|
|
175
|
+
}) => ({
|
|
176
|
+
id: params.id,
|
|
177
|
+
tab:
|
|
178
|
+
searchParams.get(
|
|
179
|
+
"tab"
|
|
180
|
+
),
|
|
181
|
+
role:
|
|
182
|
+
guardData.role,
|
|
183
|
+
}),
|
|
184
|
+
{
|
|
185
|
+
params: {
|
|
186
|
+
id: "7",
|
|
187
|
+
},
|
|
188
|
+
url:
|
|
189
|
+
"/users/7?tab=profile",
|
|
190
|
+
guardData: {
|
|
191
|
+
role: "admin",
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The result is the actual `PageLoaderExecution`, including `loaderData`, `guardData`, `hasPageLoader`, `hasGuard` and a possible Response short-circuit.
|
|
198
|
+
|
|
199
|
+
## Form action testing
|
|
200
|
+
|
|
201
|
+
`runTestPageAction()` calls the real `executePageActionFunction()` runtime.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const result =
|
|
205
|
+
await runTestPageAction(
|
|
206
|
+
async (
|
|
207
|
+
formData,
|
|
208
|
+
context
|
|
209
|
+
) => ({
|
|
210
|
+
name:
|
|
211
|
+
formData.get(
|
|
212
|
+
"name"
|
|
213
|
+
),
|
|
214
|
+
method:
|
|
215
|
+
context.method,
|
|
216
|
+
userId:
|
|
217
|
+
context.params.userId,
|
|
218
|
+
}),
|
|
219
|
+
{
|
|
220
|
+
actionName:
|
|
221
|
+
"updateUser",
|
|
222
|
+
method: "PATCH",
|
|
223
|
+
params: {
|
|
224
|
+
userId: "42",
|
|
225
|
+
},
|
|
226
|
+
fields: {
|
|
227
|
+
name: "BCP",
|
|
228
|
+
tag: [
|
|
229
|
+
"framework",
|
|
230
|
+
"testing",
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Use `fields` for simple form construction or provide an existing `FormData` through `formData`. Specifying both is rejected.
|
|
238
|
+
|
|
239
|
+
`createTestFormData()` is also exported for repeated/multipart-like field setup:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const formData =
|
|
243
|
+
createTestFormData({
|
|
244
|
+
role: [
|
|
245
|
+
"admin",
|
|
246
|
+
"developer",
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Action method validation, JSON-safe action data validation and Response short-circuit behavior all come from the production Form Action runtime.
|
|
252
|
+
|
|
253
|
+
## Response assertions
|
|
254
|
+
|
|
255
|
+
`expectResponse()` provides lightweight assertions without coupling BCP to a test framework.
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
await expectResponse(response)
|
|
259
|
+
.status(200)
|
|
260
|
+
.header(
|
|
261
|
+
"content-type",
|
|
262
|
+
/application\/json/
|
|
263
|
+
)
|
|
264
|
+
.json({
|
|
265
|
+
ok: true,
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Available assertions:
|
|
270
|
+
|
|
271
|
+
```text
|
|
272
|
+
status()
|
|
273
|
+
header()
|
|
274
|
+
text()
|
|
275
|
+
json()
|
|
276
|
+
jsonMatches()
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`jsonMatches()` checks only the provided object keys.
|
|
280
|
+
|
|
281
|
+
## Signed auth sessions
|
|
282
|
+
|
|
283
|
+
`createTestAuthSession()` creates the same signed HS256 session token used by BCP Auth.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const session =
|
|
287
|
+
await createTestAuthSession(
|
|
288
|
+
{
|
|
289
|
+
id: 42,
|
|
290
|
+
role: "admin",
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
secret:
|
|
294
|
+
process.env.BCP_SESSION_SECRET,
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
app.setCookie(
|
|
299
|
+
session.cookieName,
|
|
300
|
+
session.token
|
|
301
|
+
);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
When an `AuthSessionStore` is provided, the helper also writes the matching server-side session record.
|
|
305
|
+
|
|
306
|
+
Supported options include `sid`, session data, `expiresIn`, issuer, audience, custom cookie name, custom ID factory and `AuthSessionStore`.
|
|
307
|
+
|
|
308
|
+
This allows auth/role/permission route tests to use real BCP session verification rather than a fake user header.
|
|
309
|
+
|
|
310
|
+
## Rollback database tests
|
|
311
|
+
|
|
312
|
+
Use `withTestTransaction()` to run a test inside a real BCP database transaction and force rollback after the callback succeeds.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const result =
|
|
316
|
+
await withTestTransaction(
|
|
317
|
+
db,
|
|
318
|
+
async tx => {
|
|
319
|
+
await tx.execute(
|
|
320
|
+
"INSERT INTO users (name) VALUES (?)",
|
|
321
|
+
["Test User"]
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return tx.query(
|
|
325
|
+
"SELECT * FROM users"
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The callback result is returned to the test, but BCP deliberately throws an internal rollback signal before the transaction can commit. If the callback itself throws, the original error is preserved.
|
|
332
|
+
|
|
333
|
+
This helper assumes the configured database adapter rolls back a transaction when its callback rejects, which is the BCP database adapter contract.
|
|
334
|
+
|
|
335
|
+
## Middleware tests
|
|
336
|
+
|
|
337
|
+
`runTestMiddleware()` executes the real Middleware System v2 onion pipeline.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const response =
|
|
341
|
+
await runTestMiddleware(
|
|
342
|
+
middlewareModule,
|
|
343
|
+
{
|
|
344
|
+
url:
|
|
345
|
+
"http://bcp.test/dashboard",
|
|
346
|
+
},
|
|
347
|
+
request =>
|
|
348
|
+
Response.json({
|
|
349
|
+
header:
|
|
350
|
+
request.headers.get(
|
|
351
|
+
"x-middleware"
|
|
352
|
+
),
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
This supports matcher logic, request/response headers, redirects, rewrites, middleware state and `await next()` behavior through the framework runtime.
|
|
358
|
+
|
|
359
|
+
## Fake clock
|
|
360
|
+
|
|
361
|
+
Use `createFakeClock()` when testing jobs, schedulers, workflows or other time-sensitive code.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
const clock =
|
|
365
|
+
createFakeClock(1_000);
|
|
366
|
+
|
|
367
|
+
clock.now();
|
|
368
|
+
clock.advance(500);
|
|
369
|
+
clock.set(10_000);
|
|
370
|
+
clock.reset();
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Pass `clock.now` to runtime options that accept a `now` function.
|
|
374
|
+
|
|
375
|
+
## Deterministic IDs
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const idFactory =
|
|
379
|
+
createSequenceIdFactory(
|
|
380
|
+
"job",
|
|
381
|
+
1
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
idFactory(); // job-1
|
|
385
|
+
idFactory(); // job-2
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
This is useful for exact event/job/run assertions.
|
|
389
|
+
|
|
390
|
+
## Background job harness
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
const jobs =
|
|
394
|
+
createJobTestHarness(
|
|
395
|
+
queue
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
await jobs.drain();
|
|
399
|
+
|
|
400
|
+
await jobs.expectCount(
|
|
401
|
+
1,
|
|
402
|
+
{
|
|
403
|
+
name: "email.welcome",
|
|
404
|
+
state: "succeeded",
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Available helpers are `drain()`, `records()`, `count()` and `expectCount()`.
|
|
410
|
+
|
|
411
|
+
`drain()` processes currently eligible jobs synchronously by calling the queue's real `processNext()` API until the queue is idle or `maxJobs` is reached. Delayed jobs remain pending until the queue clock reaches their `availableAt` value.
|
|
412
|
+
|
|
413
|
+
## Workflow harness
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const workflows =
|
|
417
|
+
createWorkflowTestHarness(
|
|
418
|
+
onboarding
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const run =
|
|
422
|
+
await workflows.startAndRun({
|
|
423
|
+
userId: 42,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await workflows.expectState(
|
|
427
|
+
run.id,
|
|
428
|
+
"succeeded"
|
|
429
|
+
);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`runUntilIdle()` continues a workflow until it becomes terminal or reaches a persisted waiting state. For delay-oriented tests, `forceWaiting: true` can resume waiting workflows immediately.
|
|
433
|
+
|
|
434
|
+
## Transactional outbox harness
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
const events =
|
|
438
|
+
createOutboxTestHarness(
|
|
439
|
+
outboxStore,
|
|
440
|
+
dispatcher
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
await events.dispatchUntilIdle();
|
|
444
|
+
|
|
445
|
+
await events.expectState(
|
|
446
|
+
eventId,
|
|
447
|
+
"published"
|
|
448
|
+
);
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The harness uses the real `OutboxStore` and `OutboxDispatcher`; it does not bypass leases, retry logic or delivery state transitions.
|
|
452
|
+
|
|
453
|
+
`dispatchUntilIdle()` stops when a batch publishes zero events, so retry-delayed failures are not artificially time-skipped.
|
|
454
|
+
|
|
455
|
+
## Realtime test socket
|
|
456
|
+
|
|
457
|
+
`createRealtimeTestSocket()` implements the provider-neutral `RealtimeSocket` contract entirely in memory.
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
const socket =
|
|
461
|
+
createRealtimeTestSocket();
|
|
462
|
+
|
|
463
|
+
await realtime.attachSocket(
|
|
464
|
+
socket
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
await socket.receive({
|
|
468
|
+
type: "join",
|
|
469
|
+
channel: "orders:42",
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Inspect server-to-client messages with `socket.sent()` / `socket.messages()`. Simulate client close/error events with `closeFromClient()` and `fail()`.
|
|
474
|
+
|
|
475
|
+
## Realtime harness
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const realtimeTest =
|
|
479
|
+
createRealtimeTestHarness(
|
|
480
|
+
realtime
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const {
|
|
484
|
+
socket,
|
|
485
|
+
connection,
|
|
486
|
+
} =
|
|
487
|
+
await realtimeTest.connect();
|
|
488
|
+
|
|
489
|
+
await connection.join(
|
|
490
|
+
"orders:42"
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
await realtime.broadcast(
|
|
494
|
+
"orders:42",
|
|
495
|
+
"order.updated",
|
|
496
|
+
{
|
|
497
|
+
status: "paid",
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const event =
|
|
502
|
+
realtimeTest.expectEvent(
|
|
503
|
+
socket,
|
|
504
|
+
"order.updated",
|
|
505
|
+
"orders:42"
|
|
506
|
+
);
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
The harness uses the actual `RealtimeHub`, channel membership and broker dispatch path.
|
|
510
|
+
|
|
511
|
+
## SSE testing
|
|
512
|
+
|
|
513
|
+
`readSseEvents()` reads structured events from a Server-Sent Events response.
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
const response =
|
|
517
|
+
realtime.sse(
|
|
518
|
+
"workflow:42"
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
await realtime.broadcast(
|
|
522
|
+
"workflow:42",
|
|
523
|
+
"workflow.progress",
|
|
524
|
+
{
|
|
525
|
+
progress: 50,
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const [event] =
|
|
530
|
+
await readSseEvents(
|
|
531
|
+
response,
|
|
532
|
+
{
|
|
533
|
+
limit: 1,
|
|
534
|
+
timeoutMs: 1_000,
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
SSE comments and retry frames are ignored; `data:` is JSON-decoded when possible.
|
|
540
|
+
|
|
541
|
+
## Test runner neutrality
|
|
542
|
+
|
|
543
|
+
BCP does not depend on Jest or Vitest.
|
|
544
|
+
|
|
545
|
+
Recommended built-in setup:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
import assert from "node:assert/strict";
|
|
549
|
+
import {
|
|
550
|
+
test,
|
|
551
|
+
} from "node:test";
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
All `bcp/testing` helpers can also be used from another runner.
|
|
555
|
+
|
|
556
|
+
## Production boundary
|
|
557
|
+
|
|
558
|
+
`bcp/testing` is intended for tests and development tooling. It is server-only and the BCP client-boundary validator rejects it from pages/client islands.
|
|
559
|
+
|
|
560
|
+
Applications should avoid importing `bcp/testing` from production request modules unless the deployed code intentionally includes a test-only route or diagnostic surface.
|
|
561
|
+
|
|
562
|
+
## 0.2.14 scope
|
|
563
|
+
|
|
564
|
+
Included:
|
|
565
|
+
|
|
566
|
+
```text
|
|
567
|
+
Request/Response test app
|
|
568
|
+
cookie jar and default headers
|
|
569
|
+
API route module testing
|
|
570
|
+
page guard harness
|
|
571
|
+
page loader harness
|
|
572
|
+
form action harness
|
|
573
|
+
FormData helper
|
|
574
|
+
response assertions
|
|
575
|
+
real signed BCP auth sessions
|
|
576
|
+
optional AuthSessionStore registration
|
|
577
|
+
rollback transaction helper
|
|
578
|
+
real middleware pipeline runner
|
|
579
|
+
fake clock
|
|
580
|
+
deterministic ID factory
|
|
581
|
+
background job harness
|
|
582
|
+
workflow harness
|
|
583
|
+
outbox harness
|
|
584
|
+
fake realtime socket
|
|
585
|
+
realtime harness
|
|
586
|
+
SSE reader
|
|
587
|
+
compiled testing.mjs package runtime
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Not included in this milestone:
|
|
591
|
+
|
|
592
|
+
```text
|
|
593
|
+
browser DOM testing
|
|
594
|
+
Playwright wrapper
|
|
595
|
+
React component renderer
|
|
596
|
+
snapshot framework
|
|
597
|
+
coverage runner
|
|
598
|
+
Jest/Vitest dependency
|
|
599
|
+
containerized database provisioning
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Those can be layered on top without changing the base `bcp/testing` contract.
|