@fluojs/testing 1.0.0-beta.4 → 1.0.1

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
@@ -23,7 +23,7 @@ fluo 애플리케이션을 위한 기본 request-level 테스트 헬퍼, 모듈
23
23
  pnpm add -D @fluojs/testing vitest
24
24
  ```
25
25
 
26
- `vitest`는 mock 헬퍼와 `@fluojs/testing/vitest` 엔트리포인트가 요구하는 peer dependency입니다.
26
+ `vitest`는 mock 헬퍼와 `@fluojs/testing/vitest` 엔트리포인트가 요구하는 peer dependency입니다. `@babel/core`는 Vitest decorators plugin이 사용하는 워크스페이스의 Babel을 로드하기 때문에 peer로 선언되어 있습니다. 따라서 non-Vitest harness subpath만 사용하더라도 패키지 매니저가 해당 peer 경고를 표시할 수 있습니다.
27
27
 
28
28
  `@fluojs/testing/vitest`를 사용할 때는 `fluoBabelDecoratorsPlugin()`이 런타임에 Babel을 호출하므로, 사용하는 워크스페이스에 `@babel/core`도 함께 설치해야 합니다. Vitest 플러그인은 Vite query/hash suffix를 제거한 뒤 `.ts`, `.tsx`, `.mts`, `.cts` 소스 id를 변환하고, `node_modules`는 건너뛰며, 가장 가까운 root Babel config인 `babel.config.cjs`, `babel.config.mjs`, `babel.config.js`, `babel.config.json`을 해석합니다.
29
29
 
@@ -134,9 +134,11 @@ 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를 발생시킵니다.
137
+ 이식성 하니스의 cleanup도 계약에 포함됩니다. 앱이 bootstrap된 뒤 setup, `listen()`, assertion이 실패하면 하니스는 해당 partial app을 닫습니다. `app.close()`가 실패하면 하니스는 cleanup 실패를 보고하며, setup 또는 assertion이 이미 실패한 경우에는 원래 실패와 cleanup 실패를 모두 보존하는 aggregate error를 발생시킵니다.
138
138
 
139
- `HttpAdapterPortabilityHarness` 메서드는 공개 어댑터 계약 체크입니다. 직접 같은 검증을 다시 만들기보다 `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, `assertRemovesShutdownSignalListenersAfterClose()`처럼 초점이 분명한 assertion을 사용하세요.
139
+ `HttpAdapterPortabilityHarness`와 web-runtime portability harness 메서드는 공개 어댑터 계약 체크입니다. 직접 같은 검증을 다시 만들기보다 `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, `assertRemovesShutdownSignalListenersAfterClose()`처럼 초점이 분명한 assertion을 사용하세요.
140
+
141
+ HTTP 어댑터가 런타임 전반에서 `rawBody`의 byte-sensitive payload byte를 그대로 보존하는지 증명해야 할 때는 `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`를 사용하세요.
140
142
 
141
143
  ## canonical TDD ladder
142
144
 
package/README.md CHANGED
@@ -21,7 +21,7 @@ Default request-level testing helpers, testing module construction, and provider
21
21
  npm install --save-dev @fluojs/testing vitest
22
22
  ```
23
23
 
24
- `vitest` is a required peer dependency for the mock helpers and the `@fluojs/testing/vitest` entrypoint.
24
+ `vitest` is a required peer dependency for the mock helpers and the `@fluojs/testing/vitest` entrypoint. `@babel/core` is declared as a peer because the Vitest decorators plugin loads Babel from the consuming workspace; package managers may surface that peer even when you only use the non-Vitest harness subpaths.
25
25
 
26
26
  If you use `@fluojs/testing/vitest`, install `@babel/core` in the consuming workspace as well because `fluoBabelDecoratorsPlugin()` invokes Babel at runtime. The Vitest plugin transforms `.ts`, `.tsx`, `.mts`, and `.cts` source ids after removing Vite query/hash suffixes, skips `node_modules`, and resolves the nearest root Babel config named `babel.config.cjs`, `babel.config.mjs`, `babel.config.js`, or `babel.config.json`:
27
27
 
@@ -132,9 +132,11 @@ 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.
135
+ Portability harness cleanup is part of the contract: if setup, `listen()`, or an assertion fails after an app has been bootstrapped, the harness closes that partial app. If `app.close()` fails, the harness reports that cleanup failure, and when setup or an assertion already failed it raises an aggregate error that preserves both the original failure and the cleanup failure.
136
136
 
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.
137
+ `HttpAdapterPortabilityHarness` and web-runtime portability harness 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.
138
+
139
+ Use `assertPreservesExactRawBodyBytesForByteSensitivePayloads()` when an HTTP adapter must prove `rawBody` keeps byte-sensitive payload bytes intact across runtimes.
138
140
 
139
141
  ## Canonical TDD Ladder
140
142
 
@@ -1 +1 @@
1
- {"version":3,"file":"platform-conformance.d.ts","sourceRoot":"","sources":["../../src/conformance/platform-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACzB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,UAAU,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,qCAAqC;IACpD,OAAO,CAAC,EAAE,CACR,SAAS,EAAE,iBAAiB,EAC5B,UAAU,EAAE,wBAAwB,KACjC,YAAY,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAAC;IACtD,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2BAA2B,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,kCAAkC;IACjD,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IACrD,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD,4BAA4B,CAAC,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC;IACvF,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,WAAW,CAAC,EAAE,qCAAqC,CAAC;IACpD,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,2BAA2B,CAAC;QACtC,MAAM,EAAE,2BAA2B,CAAC;KACrC,CAAC;IACF,QAAQ,CAAC,EAAE,kCAAkC,CAAC;CAC/C;AA4ED;;GAEG;AACH,qBAAa,0BAA0B;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iCAAiC;IAEjE,yCAAyC,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B1D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3C,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCvC,2CAA2C,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B5D,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCxC,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBxC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAQjC;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
1
+ {"version":3,"file":"platform-conformance.d.ts","sourceRoot":"","sources":["../../src/conformance/platform-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACzB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,UAAU,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,qCAAqC;IACpD,OAAO,CAAC,EAAE,CACR,SAAS,EAAE,iBAAiB,EAC5B,UAAU,EAAE,wBAAwB,KACjC,YAAY,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAAC;IACtD,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2BAA2B,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,kCAAkC;IACjD,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IACrD,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD,4BAA4B,CAAC,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC;IACvF,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,WAAW,CAAC,EAAE,qCAAqC,CAAC;IACpD,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,2BAA2B,CAAC;QACtC,MAAM,EAAE,2BAA2B,CAAC;KACrC,CAAC;IACF,QAAQ,CAAC,EAAE,kCAAkC,CAAC;CAC/C;AAmGD;;GAEG;AACH,qBAAa,0BAA0B;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iCAAiC;IAEjE,yCAAyC,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B1D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B3C,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCvC,2CAA2C,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC5D,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4CxC,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBxC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAQjC;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
@@ -48,6 +48,19 @@ async function captureOutcome(action) {
48
48
  };
49
49
  }
50
50
  }
51
+ async function runCleanupWithAssertionContext(cleanup, cleanupLabel, assertionError) {
52
+ try {
53
+ await cleanup();
54
+ } catch (cleanupError) {
55
+ if (assertionError !== undefined) {
56
+ throw new AggregateError([assertionError, cleanupError], `${cleanupLabel} failed while preserving the original conformance assertion failure.`);
57
+ }
58
+ throw new AggregateError([cleanupError], `${cleanupLabel} failed during platform conformance cleanup.`);
59
+ }
60
+ if (assertionError !== undefined) {
61
+ throw assertionError;
62
+ }
63
+ }
51
64
  function collectForbiddenKeyPaths(value, patterns, allowPatterns, currentPath, violations) {
52
65
  if (Array.isArray(value)) {
53
66
  value.forEach((entry, index) => {
@@ -97,20 +110,25 @@ export class PlatformConformanceHarness {
97
110
  async assertStartIsDeterministic() {
98
111
  const component = this.options.createComponent();
99
112
  const compare = this.options.snapshot?.compare ?? defaultCompare;
100
- const firstStart = await captureOutcome(() => component.start());
101
- const firstSnapshot = component.snapshot();
102
- const secondStart = await captureOutcome(() => component.start());
103
- const secondSnapshot = component.snapshot();
104
- if (firstStart.ok !== secondStart.ok) {
105
- throw new Error('start() is not deterministic: first and second calls had different outcomes.');
106
- }
107
- if (!firstStart.ok && !secondStart.ok && firstStart.message !== secondStart.message) {
108
- throw new Error('start() rejection messages changed across duplicate calls.');
109
- }
110
- if (firstStart.ok && secondStart.ok && !compare(firstSnapshot, secondSnapshot)) {
111
- throw new Error('start() is not idempotent: duplicate calls changed component snapshot output.');
113
+ let assertionError;
114
+ try {
115
+ const firstStart = await captureOutcome(() => component.start());
116
+ const firstSnapshot = component.snapshot();
117
+ const secondStart = await captureOutcome(() => component.start());
118
+ const secondSnapshot = component.snapshot();
119
+ if (firstStart.ok !== secondStart.ok) {
120
+ throw new Error('start() is not deterministic: first and second calls had different outcomes.');
121
+ }
122
+ if (!firstStart.ok && !secondStart.ok && firstStart.message !== secondStart.message) {
123
+ throw new Error('start() rejection messages changed across duplicate calls.');
124
+ }
125
+ if (firstStart.ok && secondStart.ok && !compare(firstSnapshot, secondSnapshot)) {
126
+ throw new Error('start() is not idempotent: duplicate calls changed component snapshot output.');
127
+ }
128
+ } catch (error) {
129
+ assertionError = error;
112
130
  }
113
- await captureOutcome(() => component.stop());
131
+ await runCleanupWithAssertionContext(() => component.stop(), 'stop() after start() determinism check', assertionError);
114
132
  }
115
133
  async assertStopIsIdempotent() {
116
134
  const component = this.options.createComponent();
@@ -145,17 +163,17 @@ export class PlatformConformanceHarness {
145
163
  }
146
164
  for (const scenario of [scenarios.degraded, scenarios.failed]) {
147
165
  const component = scenario.createComponent();
148
- await scenario.enterState(component);
149
- if (scenario.expectedState !== undefined && component.state() !== scenario.expectedState) {
150
- throw new Error(`Scenario "${scenario.name}" expected state "${scenario.expectedState}" but received "${component.state()}".`);
151
- }
152
166
  try {
167
+ await scenario.enterState(component);
168
+ if (scenario.expectedState !== undefined && component.state() !== scenario.expectedState) {
169
+ throw new Error(`Scenario "${scenario.name}" expected state "${scenario.expectedState}" but received "${component.state()}".`);
170
+ }
153
171
  component.snapshot();
154
172
  } catch (error) {
155
- throw new Error(`snapshot() must be safe in "${scenario.name}" state: ${toErrorMessage(error)}`);
156
- } finally {
157
- await captureOutcome(() => component.stop());
173
+ const assertionError = error instanceof Error && error.message.startsWith('Scenario ') ? error : new Error(`snapshot() must be safe in "${scenario.name}" state: ${toErrorMessage(error)}`);
174
+ await runCleanupWithAssertionContext(() => component.stop(), `stop() after "${scenario.name}" snapshot scenario`, assertionError);
158
175
  }
176
+ await runCleanupWithAssertionContext(() => component.stop(), `stop() after "${scenario.name}" snapshot scenario`);
159
177
  }
160
178
  }
161
179
  async assertStableDiagnostics() {
@@ -169,10 +187,10 @@ export class PlatformConformanceHarness {
169
187
  const requiredFixHintSeverities = this.options.diagnostics?.requireFixHintForSeverities ?? DEFAULT_REQUIRED_FIX_HINT_SEVERITIES;
170
188
  for (const issue of diagnostics) {
171
189
  if (issue.code.trim().length === 0) {
172
- throw new Error('Diagnostics must provide a stable non-empty code.');
190
+ throw new Error(`Diagnostics must provide a stable non-empty code. Received message: ${issue.message}`);
173
191
  }
174
192
  if (requiredFixHintSeverities.includes(issue.severity) && (!issue.fixHint || issue.fixHint.trim().length === 0)) {
175
- throw new Error(`Diagnostic ${issue.code} (${issue.severity}) must provide a fixHint.`);
193
+ throw new Error(`Diagnostic ${issue.code} (${issue.severity}) must provide a fixHint. Message: ${issue.message}`);
176
194
  }
177
195
  }
178
196
  const expectedCodes = this.options.diagnostics?.expectedCodes;
@@ -183,7 +201,8 @@ export class PlatformConformanceHarness {
183
201
  const actualCodes = normalizeCodes(diagnostics.map(diagnostic => diagnostic.code));
184
202
  const normalizedExpectedCodes = normalizeCodes(expectedCodes);
185
203
  if (!defaultCompare(actualCodes, normalizedExpectedCodes)) {
186
- throw new Error(`Diagnostic code set changed. Expected [${normalizedExpectedCodes.join(', ')}] but received [${actualCodes.join(', ')}].`);
204
+ const actualDetails = diagnostics.map(diagnostic => `${diagnostic.code}(${diagnostic.severity}): ${diagnostic.message}`).join('; ');
205
+ throw new Error(`Diagnostic code set changed. Expected [${normalizedExpectedCodes.join(', ')}] but received [${actualCodes.join(', ')}]. Actual diagnostics: ${actualDetails}`);
187
206
  }
188
207
  }
189
208
  async assertSnapshotSanitized() {
@@ -1 +1 @@
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"}
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;AAuGD;;;;;;;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"}
@@ -78,6 +78,19 @@ async function runWithCleanup(app, adapterName, assertion) {
78
78
  throw assertionError;
79
79
  }
80
80
  }
81
+ async function prepareAndListenWithCleanup(app, adapterName, prepare) {
82
+ try {
83
+ await prepare?.();
84
+ await app.listen();
85
+ } catch (setupError) {
86
+ try {
87
+ await app.close();
88
+ } catch (cleanupError) {
89
+ throw new AggregateError([setupError, cleanupError], `${adapterName} adapter portability setup failed and app.close() also failed during harness cleanup.`);
90
+ }
91
+ throw setupError;
92
+ }
93
+ }
81
94
 
82
95
  /**
83
96
  * A portability harness for testing HTTP adapters to ensure they behave
@@ -130,7 +143,7 @@ export class HttpAdapterPortabilityHarness {
130
143
  cors: false,
131
144
  port
132
145
  });
133
- await app.listen();
146
+ await prepareAndListenWithCleanup(app, this.options.name);
134
147
  await runWithCleanup(app, this.options.name, async () => {
135
148
  const response = await fetch(`http://127.0.0.1:${String(port)}/cookies`, {
136
149
  headers: {
@@ -185,7 +198,7 @@ export class HttpAdapterPortabilityHarness {
185
198
  port,
186
199
  rawBody: true
187
200
  });
188
- await app.listen();
201
+ await prepareAndListenWithCleanup(app, this.options.name);
189
202
  await runWithCleanup(app, this.options.name, async () => {
190
203
  const [jsonResponse, textResponse] = await Promise.all([fetch(`http://127.0.0.1:${String(port)}/webhooks/json`, {
191
204
  body: JSON.stringify({
@@ -254,8 +267,9 @@ export class HttpAdapterPortabilityHarness {
254
267
  port,
255
268
  rawBody: true
256
269
  });
257
- await this.options.prepareExactRawBodyByteTest?.(app);
258
- await app.listen();
270
+ await prepareAndListenWithCleanup(app, this.options.name, async () => {
271
+ await this.options.prepareExactRawBodyByteTest?.(app);
272
+ });
259
273
  await runWithCleanup(app, this.options.name, async () => {
260
274
  const payload = Uint8Array.from([0xe9, 0x41]);
261
275
  const contentType = this.options.exactRawBodyByteContentType ?? 'text/plain; charset=latin1';
@@ -311,7 +325,7 @@ export class HttpAdapterPortabilityHarness {
311
325
  port,
312
326
  rawBody: true
313
327
  });
314
- await app.listen();
328
+ await prepareAndListenWithCleanup(app, this.options.name);
315
329
  await runWithCleanup(app, this.options.name, async () => {
316
330
  const form = new FormData();
317
331
  form.set('name', 'Ada');
@@ -373,7 +387,7 @@ export class HttpAdapterPortabilityHarness {
373
387
  },
374
388
  port
375
389
  });
376
- await app.listen();
390
+ await prepareAndListenWithCleanup(app, this.options.name);
377
391
  await runWithCleanup(app, this.options.name, async () => {
378
392
  const form = new FormData();
379
393
  form.set('name', 'Ada');
@@ -433,7 +447,7 @@ export class HttpAdapterPortabilityHarness {
433
447
  cors: false,
434
448
  port
435
449
  });
436
- await app.listen();
450
+ await prepareAndListenWithCleanup(app, this.options.name);
437
451
  await runWithCleanup(app, this.options.name, async () => {
438
452
  const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
439
453
  headers: {
@@ -501,7 +515,7 @@ export class HttpAdapterPortabilityHarness {
501
515
  cors: false,
502
516
  port
503
517
  });
504
- await app.listen();
518
+ await prepareAndListenWithCleanup(app, this.options.name);
505
519
  await runWithCleanup(app, this.options.name, async () => {
506
520
  const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
507
521
  headers: {
@@ -25,6 +25,10 @@ export declare class WebRuntimeHttpAdapterPortabilityHarness<TBootstrapOptions e
25
25
  assertPreservesQueryArraysAndDecoding(): Promise<void>;
26
26
  assertPreservesMalformedCookieValues(): Promise<void>;
27
27
  assertPreservesRawBodyForJsonAndText(): Promise<void>;
28
+ /**
29
+ * Asserts that byte-sensitive request bodies preserve their exact raw bytes.
30
+ */
31
+ assertPreservesExactRawBodyBytesForByteSensitivePayloads(): Promise<void>;
28
32
  assertExcludesRawBodyForMultipart(): Promise<void>;
29
33
  assertSupportsSseStreaming(): Promise<void>;
30
34
  }
@@ -1 +1 @@
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"}
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;IAsE3D;;OAEG;IACG,wDAAwD,IAAI,OAAO,CAAC,IAAI,CAAC;IA0CzE,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"}
@@ -195,18 +195,72 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
195
195
  }
196
196
  });
197
197
  }
198
- async assertExcludesRawBodyForMultipart() {
198
+
199
+ /**
200
+ * Asserts that byte-sensitive request bodies preserve their exact raw bytes.
201
+ */
202
+ async assertPreservesExactRawBodyBytesForByteSensitivePayloads() {
199
203
  let _initProto4, _initClass4;
204
+ let _WebhookController2;
205
+ class WebhookController {
206
+ static {
207
+ ({
208
+ e: [_initProto4],
209
+ c: [_WebhookController2, _initClass4]
210
+ } = _applyDecs(this, [Controller('/webhooks')], [[Post('/bytes'), 2, "handleBytes"]]));
211
+ }
212
+ constructor() {
213
+ _initProto4(this);
214
+ }
215
+ handleBytes(_input, context) {
216
+ return {
217
+ rawBytes: Array.from(context.request.rawBody ?? new Uint8Array())
218
+ };
219
+ }
220
+ static {
221
+ _initClass4();
222
+ }
223
+ }
224
+ class AppModule {}
225
+ defineModule(AppModule, {
226
+ controllers: [_WebhookController2]
227
+ });
228
+ const app = await this.options.bootstrap(AppModule, {
229
+ cors: false,
230
+ rawBody: true
231
+ });
232
+ await runWithCleanup(app, this.options.name, async () => {
233
+ const payload = Uint8Array.from([0xe9, 0x41]);
234
+ const response = await app.dispatch(new Request('https://runtime.test/webhooks/bytes', {
235
+ body: payload,
236
+ headers: {
237
+ 'content-type': 'application/octet-stream'
238
+ },
239
+ method: 'POST'
240
+ }));
241
+ if (response.status !== 201) {
242
+ throw new Error(`${this.options.name} adapter changed byte-sensitive rawBody response status semantics.`);
243
+ }
244
+ const body = await response.json();
245
+ if (JSON.stringify(body) !== JSON.stringify({
246
+ rawBytes: Array.from(payload)
247
+ })) {
248
+ throw new Error(`${this.options.name} adapter changed exact-byte rawBody semantics.`);
249
+ }
250
+ });
251
+ }
252
+ async assertExcludesRawBodyForMultipart() {
253
+ let _initProto5, _initClass5;
200
254
  let _UploadController;
201
255
  class UploadController {
202
256
  static {
203
257
  ({
204
- e: [_initProto4],
205
- c: [_UploadController, _initClass4]
258
+ e: [_initProto5],
259
+ c: [_UploadController, _initClass5]
206
260
  } = _applyDecs(this, [Controller('/uploads')], [[Post('/'), 2, "upload"]]));
207
261
  }
208
262
  constructor() {
209
- _initProto4(this);
263
+ _initProto5(this);
210
264
  }
211
265
  upload(_input, context) {
212
266
  return {
@@ -216,7 +270,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
216
270
  };
217
271
  }
218
272
  static {
219
- _initClass4();
273
+ _initClass5();
220
274
  }
221
275
  }
222
276
  class AppModule {}
@@ -253,17 +307,17 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
253
307
  });
254
308
  }
255
309
  async assertSupportsSseStreaming() {
256
- let _initProto5, _initClass5;
310
+ let _initProto6, _initClass6;
257
311
  let _EventsController;
258
312
  class EventsController {
259
313
  static {
260
314
  ({
261
- e: [_initProto5],
262
- c: [_EventsController, _initClass5]
315
+ e: [_initProto6],
316
+ c: [_EventsController, _initClass6]
263
317
  } = _applyDecs(this, [Controller('/events')], [[Get('/'), 2, "stream"]]));
264
318
  }
265
319
  constructor() {
266
- _initProto5(this);
320
+ _initProto6(this);
267
321
  }
268
322
  stream(_input, context) {
269
323
  const stream = new SseResponse(context);
@@ -278,7 +332,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
278
332
  return stream;
279
333
  }
280
334
  static {
281
- _initClass5();
335
+ _initClass6();
282
336
  }
283
337
  }
284
338
  class AppModule {}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "override",
10
10
  "module-builder"
11
11
  ],
12
- "version": "1.0.0-beta.4",
12
+ "version": "1.0.1",
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/config": "^1.0.0-beta.8",
80
- "@fluojs/core": "^1.0.0-beta.6",
81
- "@fluojs/http": "^1.0.0-beta.11",
82
- "@fluojs/runtime": "^1.0.0-beta.12",
83
- "@fluojs/di": "^1.0.0-beta.8"
79
+ "@fluojs/config": "^1.0.1",
80
+ "@fluojs/core": "^1.0.1",
81
+ "@fluojs/http": "^1.0.0",
82
+ "@fluojs/di": "^1.0.1",
83
+ "@fluojs/runtime": "^1.0.1"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "@babel/core": ">=7.0.0",
@@ -88,12 +88,12 @@
88
88
  },
89
89
  "devDependencies": {
90
90
  "vitest": "^3.2.4",
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",
94
- "@fluojs/platform-nodejs": "^1.0.0-beta.5",
95
- "@fluojs/platform-express": "^1.0.0-beta.7",
96
- "@fluojs/platform-fastify": "^1.0.0-beta.8"
91
+ "@fluojs/platform-bun": "^1.0.1",
92
+ "@fluojs/platform-cloudflare-workers": "^1.0.1",
93
+ "@fluojs/platform-deno": "^1.0.1",
94
+ "@fluojs/platform-nodejs": "^1.0.1",
95
+ "@fluojs/platform-express": "^1.0.1",
96
+ "@fluojs/platform-fastify": "^1.0.1"
97
97
  },
98
98
  "scripts": {
99
99
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",