@fluojs/testing 1.0.0-beta.3 → 1.0.0-beta.4

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.ko.md CHANGED
@@ -134,6 +134,8 @@ const mailer = createDeepMock(MailService);
134
134
 
135
135
  프레임워크 지향 플랫폼 패키지를 작성할 때는 `@fluojs/testing/platform-conformance`, `@fluojs/testing/http-adapter-portability`, `@fluojs/testing/web-runtime-adapter-portability` 같은 서브패스를 사용해 적합성 및 이식성 검증을 수행합니다.
136
136
 
137
+ 이식성 하니스의 cleanup도 계약에 포함됩니다. `app.close()`가 실패하면 하니스는 cleanup 실패를 보고하며, assertion이 이미 실패한 경우에는 assertion 실패와 cleanup 실패를 모두 보존하는 aggregate error를 발생시킵니다.
138
+
137
139
  `HttpAdapterPortabilityHarness` 메서드는 공개 어댑터 계약 체크입니다. 직접 같은 검증을 다시 만들기보다 `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, `assertRemovesShutdownSignalListenersAfterClose()`처럼 초점이 분명한 assertion을 사용하세요.
138
140
 
139
141
  ## canonical TDD ladder
package/README.md CHANGED
@@ -132,6 +132,8 @@ Install `vitest` in the consuming workspace before using the mock helpers so the
132
132
 
133
133
  Use subpaths like `@fluojs/testing/platform-conformance`, `@fluojs/testing/http-adapter-portability`, and `@fluojs/testing/web-runtime-adapter-portability` when authoring framework-facing platform packages.
134
134
 
135
+ Portability harness cleanup is part of the contract: if `app.close()` fails, the harness reports that cleanup failure, and when an assertion already failed it raises an aggregate error that preserves both the assertion failure and the cleanup failure.
136
+
135
137
  `HttpAdapterPortabilityHarness` methods are the public adapter contract checks. Prefer focused assertions such as `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, and `assertRemovesShutdownSignalListenersAfterClose()` instead of hand-rolled equivalents.
136
138
 
137
139
  ## Canonical TDD Ladder
@@ -1 +1 @@
1
- {"version":3,"file":"http-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/http-adapter-portability.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED,KAAK,OAAO,GAAG;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,oCAAoC,CACnD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAE9B;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjF;;OAEG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IAErC;;OAEG;IACH,2BAA2B,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;OAMG;IACH,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE;AAuDD;;;;;;;GAOG;AACH,qBAAa,6BAA6B,CACxC,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAOlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC;;;;OAIG;gBAC0B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC;IAEhH;;;OAGG;IACG,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IA+CrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IA8DrD,wDAAwD,IAAI,OAAO,CAAC,IAAI,CAAC;IA6CzE,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IA8ClD,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;IAwD/D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IAkDjD;;;OAGG;IACG,mCAAmC,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDpD,wCAAwC,IAAI,OAAO,CAAC,IAAI,CAAC;IAuDzD,4BAA4B,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAuDjF,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;CA8CtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mCAAmC,CACjD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO,EAE9B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,GAClF,6BAA6B,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,CAErE"}
1
+ {"version":3,"file":"http-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/http-adapter-portability.ts"],"names":[],"mappings":"AAIA,OAAO,EAAwC,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE3G,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED,KAAK,OAAO,GAAG;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,oCAAoC,CACnD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAE9B;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjF;;OAEG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IAErC;;OAEG;IACH,2BAA2B,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;OAMG;IACH,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE;AAiFD;;;;;;;GAOG;AACH,qBAAa,6BAA6B,CACxC,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAOlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC;;;;OAIG;gBAC0B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC;IAEhH;;;OAGG;IACG,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAkDrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAsErD,wDAAwD,IAAI,OAAO,CAAC,IAAI,CAAC;IA+CzE,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IAuDlD,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;IAsD/D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IAmDjD;;;OAGG;IACG,mCAAmC,IAAI,OAAO,CAAC,IAAI,CAAC;IA0DpD,wCAAwC,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDzD,4BAA4B,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqDjF,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;CA4CtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mCAAmC,CACjD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO,EAE9B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,GAClF,6BAA6B,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,CAErE"}
@@ -57,10 +57,26 @@ async function requestHttps(url) {
57
57
  request.end();
58
58
  });
59
59
  }
60
- async function closeSilently(app) {
60
+ async function runWithCleanup(app, adapterName, assertion) {
61
+ let hasAssertionError = false;
62
+ let assertionError;
63
+ try {
64
+ await assertion();
65
+ } catch (error) {
66
+ hasAssertionError = true;
67
+ assertionError = error;
68
+ }
61
69
  try {
62
70
  await app.close();
63
- } catch {}
71
+ } catch (cleanupError) {
72
+ if (hasAssertionError) {
73
+ throw new AggregateError([assertionError, cleanupError], `${adapterName} adapter portability assertion failed and app.close() also failed during harness cleanup.`);
74
+ }
75
+ throw new AggregateError([cleanupError], `${adapterName} adapter app.close() failed during portability harness cleanup.`);
76
+ }
77
+ if (hasAssertionError) {
78
+ throw assertionError;
79
+ }
64
80
  }
65
81
 
66
82
  /**
@@ -115,7 +131,7 @@ export class HttpAdapterPortabilityHarness {
115
131
  port
116
132
  });
117
133
  await app.listen();
118
- try {
134
+ await runWithCleanup(app, this.options.name, async () => {
119
135
  const response = await fetch(`http://127.0.0.1:${String(port)}/cookies`, {
120
136
  headers: {
121
137
  cookie: 'good=hello%20world; bad=%E0%A4%A'
@@ -128,9 +144,7 @@ export class HttpAdapterPortabilityHarness {
128
144
  if (typeof body !== 'object' || body === null || !('bad' in body) || !('good' in body) || body.bad !== '%E0%A4%A' || body.good !== 'hello world' || Object.keys(body).length !== 2) {
129
145
  throw new Error(`${this.options.name} adapter changed malformed-cookie normalization.`);
130
146
  }
131
- } finally {
132
- await closeSilently(app);
133
- }
147
+ });
134
148
  }
135
149
  async assertPreservesRawBodyForJsonAndText() {
136
150
  let _initProto2, _initClass2;
@@ -172,7 +186,7 @@ export class HttpAdapterPortabilityHarness {
172
186
  rawBody: true
173
187
  });
174
188
  await app.listen();
175
- try {
189
+ await runWithCleanup(app, this.options.name, async () => {
176
190
  const [jsonResponse, textResponse] = await Promise.all([fetch(`http://127.0.0.1:${String(port)}/webhooks/json`, {
177
191
  body: JSON.stringify({
178
192
  provider: 'stripe'
@@ -206,9 +220,7 @@ export class HttpAdapterPortabilityHarness {
206
220
  })) {
207
221
  throw new Error(`${this.options.name} adapter changed text rawBody semantics.`);
208
222
  }
209
- } finally {
210
- await closeSilently(app);
211
- }
223
+ });
212
224
  }
213
225
  async assertPreservesExactRawBodyBytesForByteSensitivePayloads() {
214
226
  let _initProto3, _initClass3;
@@ -244,7 +256,7 @@ export class HttpAdapterPortabilityHarness {
244
256
  });
245
257
  await this.options.prepareExactRawBodyByteTest?.(app);
246
258
  await app.listen();
247
- try {
259
+ await runWithCleanup(app, this.options.name, async () => {
248
260
  const payload = Uint8Array.from([0xe9, 0x41]);
249
261
  const contentType = this.options.exactRawBodyByteContentType ?? 'text/plain; charset=latin1';
250
262
  const response = await fetch(`http://127.0.0.1:${String(port)}/webhooks/bytes`, {
@@ -263,9 +275,7 @@ export class HttpAdapterPortabilityHarness {
263
275
  })) {
264
276
  throw new Error(`${this.options.name} adapter changed exact-byte rawBody semantics.`);
265
277
  }
266
- } finally {
267
- await closeSilently(app);
268
- }
278
+ });
269
279
  }
270
280
  async assertExcludesRawBodyForMultipart() {
271
281
  let _initProto4, _initClass4;
@@ -302,7 +312,7 @@ export class HttpAdapterPortabilityHarness {
302
312
  rawBody: true
303
313
  });
304
314
  await app.listen();
305
- try {
315
+ await runWithCleanup(app, this.options.name, async () => {
306
316
  const form = new FormData();
307
317
  form.set('name', 'Ada');
308
318
  form.set('payload', new Blob(['hello'], {
@@ -325,9 +335,7 @@ export class HttpAdapterPortabilityHarness {
325
335
  })) {
326
336
  throw new Error(`${this.options.name} adapter changed multipart rawBody semantics.`);
327
337
  }
328
- } finally {
329
- await closeSilently(app);
330
- }
338
+ });
331
339
  }
332
340
  async assertDefaultsMultipartTotalLimitToMaxBodySize() {
333
341
  let _initProto5, _initClass5;
@@ -366,7 +374,7 @@ export class HttpAdapterPortabilityHarness {
366
374
  port
367
375
  });
368
376
  await app.listen();
369
- try {
377
+ await runWithCleanup(app, this.options.name, async () => {
370
378
  const form = new FormData();
371
379
  form.set('name', 'Ada');
372
380
  form.set('payload', new Blob(['12345678'], {
@@ -383,9 +391,7 @@ export class HttpAdapterPortabilityHarness {
383
391
  if (typeof body !== 'object' || body === null || body.error?.code !== 'PAYLOAD_TOO_LARGE') {
384
392
  throw new Error(`${this.options.name} adapter changed multipart limit error semantics.`);
385
393
  }
386
- } finally {
387
- await closeSilently(app);
388
- }
394
+ });
389
395
  }
390
396
  async assertSupportsSseStreaming() {
391
397
  let _initProto6, _initClass6;
@@ -428,7 +434,7 @@ export class HttpAdapterPortabilityHarness {
428
434
  port
429
435
  });
430
436
  await app.listen();
431
- try {
437
+ await runWithCleanup(app, this.options.name, async () => {
432
438
  const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
433
439
  headers: {
434
440
  accept: 'text/event-stream'
@@ -445,9 +451,7 @@ export class HttpAdapterPortabilityHarness {
445
451
  if (!body.includes('event: ready') || !body.includes('data: {"ready":true}')) {
446
452
  throw new Error(`${this.options.name} adapter changed SSE body framing.`);
447
453
  }
448
- } finally {
449
- await closeSilently(app);
450
- }
454
+ });
451
455
  }
452
456
 
453
457
  /**
@@ -498,7 +502,7 @@ export class HttpAdapterPortabilityHarness {
498
502
  port
499
503
  });
500
504
  await app.listen();
501
- try {
505
+ await runWithCleanup(app, this.options.name, async () => {
502
506
  const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
503
507
  headers: {
504
508
  accept: 'text/event-stream'
@@ -509,9 +513,7 @@ export class HttpAdapterPortabilityHarness {
509
513
  }
510
514
  await response.text();
511
515
  await withTimeout(drainWaitSettled, 2_000, `${this.options.name} adapter left response.stream.waitForDrain() pending after close.`);
512
- } finally {
513
- await closeSilently(app);
514
- }
516
+ });
515
517
  }
516
518
  async assertReportsConfiguredHostInStartupLogs() {
517
519
  let _initProto8, _initClass8;
@@ -557,7 +559,7 @@ export class HttpAdapterPortabilityHarness {
557
559
  logger,
558
560
  port
559
561
  });
560
- try {
562
+ await runWithCleanup(app, this.options.name, async () => {
561
563
  const response = await fetch(`http://127.0.0.1:${String(port)}/health`);
562
564
  if (response.status !== 200) {
563
565
  throw new Error(`${this.options.name} adapter changed host-bound health response semantics.`);
@@ -572,9 +574,7 @@ export class HttpAdapterPortabilityHarness {
572
574
  if (!loggerEvents.includes(expectedLog)) {
573
575
  throw new Error(`${this.options.name} adapter changed startup host logging.`);
574
576
  }
575
- } finally {
576
- await closeSilently(app);
577
- }
577
+ });
578
578
  }
579
579
  async assertReportsHttpsStartupUrl(https) {
580
580
  let _initProto9, _initClass9;
@@ -621,7 +621,7 @@ export class HttpAdapterPortabilityHarness {
621
621
  logger,
622
622
  port
623
623
  });
624
- try {
624
+ await runWithCleanup(app, this.options.name, async () => {
625
625
  const response = await requestHttps(`https://127.0.0.1:${String(port)}/health`);
626
626
  if (response.statusCode !== 200) {
627
627
  throw new Error(`${this.options.name} adapter changed HTTPS response status semantics.`);
@@ -635,9 +635,7 @@ export class HttpAdapterPortabilityHarness {
635
635
  if (!loggerEvents.includes(expectedLog)) {
636
636
  throw new Error(`${this.options.name} adapter changed HTTPS startup logging.`);
637
637
  }
638
- } finally {
639
- await closeSilently(app);
640
- }
638
+ });
641
639
  }
642
640
  async assertRemovesShutdownSignalListenersAfterClose() {
643
641
  let _initProto0, _initClass0;
@@ -681,13 +679,11 @@ export class HttpAdapterPortabilityHarness {
681
679
  shutdownSignals: [signal]
682
680
  });
683
681
  const registeredListeners = process.listeners(signal).filter(listener => !listenersBefore.has(listener));
684
- try {
682
+ await runWithCleanup(app, this.options.name, async () => {
685
683
  if (registeredListeners.length === 0) {
686
684
  throw new Error(`${this.options.name} adapter did not register the expected shutdown listener.`);
687
685
  }
688
- } finally {
689
- await closeSilently(app);
690
- }
686
+ });
691
687
  const remainingListeners = process.listeners(signal);
692
688
  const leakedListeners = registeredListeners.filter(listener => remainingListeners.includes(listener));
693
689
  if (leakedListeners.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"web-runtime-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/web-runtime-adapter-portability.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED,KAAK,4BAA4B,GAAG;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/C,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,8CAA8C,CAC7D,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B;IAExE,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAYD;;GAEG;AACH,qBAAa,uCAAuC,CAClD,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B;IAE5D,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,8CAA8C,CAAC,iBAAiB,EAAE,IAAI,CAAC;IAEvG,qCAAqC,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCtD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IA4CrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IA2DrD,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IA2ClD,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;CA4ClD;AAED;;;;;GAKG;AACH,wBAAgB,6CAA6C,CAC3D,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B,EAExE,OAAO,EAAE,8CAA8C,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAC/E,uCAAuC,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAElE"}
1
+ {"version":3,"file":"web-runtime-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/web-runtime-adapter-portability.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED,KAAK,4BAA4B,GAAG;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/C,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,8CAA8C,CAC7D,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B;IAExE,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AA0CD;;GAEG;AACH,qBAAa,uCAAuC,CAClD,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B;IAE5D,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,8CAA8C,CAAC,iBAAiB,EAAE,IAAI,CAAC;IAEvG,qCAAqC,IAAI,OAAO,CAAC,IAAI,CAAC;IAyCtD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAgDrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAsErD,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDlD,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;CA8ClD;AAED;;;;;GAKG;AACH,wBAAgB,6CAA6C,CAC3D,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B,EAExE,OAAO,EAAE,8CAA8C,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAC/E,uCAAuC,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAElE"}
@@ -3,9 +3,7 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
3
3
  function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
4
4
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
5
5
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
6
- // @ts-ignore Worktree-local LSP does not resolve workspace package aliases.
7
6
  import { Controller, Get, Post, SseResponse } from '@fluojs/http';
8
- // @ts-ignore Worktree-local LSP does not resolve workspace package aliases.
9
7
  import { defineModule } from '@fluojs/runtime';
10
8
 
11
9
  /**
@@ -15,10 +13,26 @@ import { defineModule } from '@fluojs/runtime';
15
13
  function decodeUtf8(input) {
16
14
  return new TextDecoder().decode(input ?? new Uint8Array());
17
15
  }
18
- async function closeSilently(app) {
16
+ async function runWithCleanup(app, adapterName, assertion) {
17
+ let hasAssertionError = false;
18
+ let assertionError;
19
+ try {
20
+ await assertion();
21
+ } catch (error) {
22
+ hasAssertionError = true;
23
+ assertionError = error;
24
+ }
19
25
  try {
20
26
  await app.close();
21
- } catch {}
27
+ } catch (cleanupError) {
28
+ if (hasAssertionError) {
29
+ throw new AggregateError([assertionError, cleanupError], `${adapterName} adapter portability assertion failed and app.close() also failed during harness cleanup.`);
30
+ }
31
+ throw new AggregateError([cleanupError], `${adapterName} adapter app.close() failed during portability harness cleanup.`);
32
+ }
33
+ if (hasAssertionError) {
34
+ throw assertionError;
35
+ }
22
36
  }
23
37
 
24
38
  /**
@@ -55,7 +69,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
55
69
  const app = await this.options.bootstrap(AppModule, {
56
70
  cors: false
57
71
  });
58
- try {
72
+ await runWithCleanup(app, this.options.name, async () => {
59
73
  const response = await app.dispatch(new Request('https://runtime.test/query?tag=one&tag=two&encoded=hello+world&flag&bad=%E0%A4%A'));
60
74
  if (response.status !== 200) {
61
75
  throw new Error(`${this.options.name} adapter changed query response status semantics.`);
@@ -64,9 +78,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
64
78
  if (typeof body !== 'object' || body === null || body.bad !== '�%A' || body.encoded !== 'hello world' || !Array.isArray(body.tag) || JSON.stringify(body.tag) !== JSON.stringify(['one', 'two'])) {
65
79
  throw new Error(`${this.options.name} adapter changed query decoding semantics.`);
66
80
  }
67
- } finally {
68
- await closeSilently(app);
69
- }
81
+ });
70
82
  }
71
83
  async assertPreservesMalformedCookieValues() {
72
84
  let _initProto2, _initClass2;
@@ -95,7 +107,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
95
107
  const app = await this.options.bootstrap(AppModule, {
96
108
  cors: false
97
109
  });
98
- try {
110
+ await runWithCleanup(app, this.options.name, async () => {
99
111
  const response = await app.dispatch(new Request('https://runtime.test/cookies', {
100
112
  headers: {
101
113
  cookie: 'good=hello%20world; bad=%E0%A4%A'
@@ -108,9 +120,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
108
120
  if (typeof body !== 'object' || body === null || !('bad' in body) || !('good' in body) || body.bad !== '%E0%A4%A' || body.good !== 'hello world' || Object.keys(body).length !== 2) {
109
121
  throw new Error(`${this.options.name} adapter changed malformed-cookie normalization.`);
110
122
  }
111
- } finally {
112
- await closeSilently(app);
113
- }
123
+ });
114
124
  }
115
125
  async assertPreservesRawBodyForJsonAndText() {
116
126
  let _initProto3, _initClass3;
@@ -149,7 +159,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
149
159
  cors: false,
150
160
  rawBody: true
151
161
  });
152
- try {
162
+ await runWithCleanup(app, this.options.name, async () => {
153
163
  const [jsonResponse, textResponse] = await Promise.all([app.dispatch(new Request('https://runtime.test/webhooks/json', {
154
164
  body: JSON.stringify({
155
165
  provider: 'stripe'
@@ -183,9 +193,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
183
193
  })) {
184
194
  throw new Error(`${this.options.name} adapter changed text rawBody semantics.`);
185
195
  }
186
- } finally {
187
- await closeSilently(app);
188
- }
196
+ });
189
197
  }
190
198
  async assertExcludesRawBodyForMultipart() {
191
199
  let _initProto4, _initClass4;
@@ -219,7 +227,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
219
227
  cors: false,
220
228
  rawBody: true
221
229
  });
222
- try {
230
+ await runWithCleanup(app, this.options.name, async () => {
223
231
  const form = new FormData();
224
232
  form.set('name', 'Ada');
225
233
  form.set('payload', new Blob(['hello'], {
@@ -242,9 +250,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
242
250
  })) {
243
251
  throw new Error(`${this.options.name} adapter changed multipart rawBody semantics.`);
244
252
  }
245
- } finally {
246
- await closeSilently(app);
247
- }
253
+ });
248
254
  }
249
255
  async assertSupportsSseStreaming() {
250
256
  let _initProto5, _initClass5;
@@ -282,7 +288,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
282
288
  const app = await this.options.bootstrap(AppModule, {
283
289
  cors: false
284
290
  });
285
- try {
291
+ await runWithCleanup(app, this.options.name, async () => {
286
292
  const response = await app.dispatch(new Request('https://runtime.test/events', {
287
293
  headers: {
288
294
  accept: 'text/event-stream'
@@ -299,9 +305,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
299
305
  if (!body.includes('event: ready') || !body.includes('data: {"ready":true}')) {
300
306
  throw new Error(`${this.options.name} adapter changed SSE body framing.`);
301
307
  }
302
- } finally {
303
- await closeSilently(app);
304
- }
308
+ });
305
309
  }
306
310
  }
307
311
 
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "override",
10
10
  "module-builder"
11
11
  ],
12
- "version": "1.0.0-beta.3",
12
+ "version": "1.0.0-beta.4",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -76,11 +76,11 @@
76
76
  "dist"
77
77
  ],
78
78
  "dependencies": {
79
- "@fluojs/core": "^1.0.0-beta.5",
80
- "@fluojs/di": "^1.0.0-beta.7",
79
+ "@fluojs/config": "^1.0.0-beta.8",
80
+ "@fluojs/core": "^1.0.0-beta.6",
81
+ "@fluojs/http": "^1.0.0-beta.11",
81
82
  "@fluojs/runtime": "^1.0.0-beta.12",
82
- "@fluojs/http": "^1.0.0-beta.10",
83
- "@fluojs/config": "^1.0.0-beta.8"
83
+ "@fluojs/di": "^1.0.0-beta.8"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "@babel/core": ">=7.0.0",
@@ -88,8 +88,11 @@
88
88
  },
89
89
  "devDependencies": {
90
90
  "vitest": "^3.2.4",
91
- "@fluojs/platform-express": "^1.0.0-beta.7",
91
+ "@fluojs/platform-bun": "^1.0.0-beta.7",
92
+ "@fluojs/platform-deno": "^1.0.0-beta.5",
93
+ "@fluojs/platform-cloudflare-workers": "^1.0.0-beta.4",
92
94
  "@fluojs/platform-nodejs": "^1.0.0-beta.5",
95
+ "@fluojs/platform-express": "^1.0.0-beta.7",
93
96
  "@fluojs/platform-fastify": "^1.0.0-beta.8"
94
97
  },
95
98
  "scripts": {