@gcoredev/fastedge-test 0.1.5 → 0.1.7

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Proxy Runner</title>
7
- <script type="module" crossorigin src="/assets/index-BpdzhbRl.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-BCXfEMSq.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-DdlINQc_.css">
9
9
  </head>
10
10
  <body>
@@ -2574,7 +2574,7 @@ var ProxyWasmRunner = class {
2574
2574
  }
2575
2575
  }
2576
2576
  createImports() {
2577
- const wasi = new import_node_wasi.WASI({ version: "preview1" });
2577
+ const wasi = new import_node_wasi.WASI({ version: "preview1", env: this.dictionary.getAll() });
2578
2578
  const wasiImport = wasi.wasiImport;
2579
2579
  return {
2580
2580
  env: this.hostFunctions.createImports(),
@@ -2861,6 +2861,8 @@ var HttpWasmRunner = class {
2861
2861
  this.stateManager = null;
2862
2862
  this.dotenvEnabled = true;
2863
2863
  this.dotenvPath = null;
2864
+ /** Pinned ports bypass PortManager allocation and must not be released back to it. */
2865
+ this.isPinnedPort = false;
2864
2866
  /** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
2865
2867
  this.isLegacySync = false;
2866
2868
  this.portManager = portManager;
@@ -2887,7 +2889,19 @@ var HttpWasmRunner = class {
2887
2889
  this.tempWasmPath = wasmPath;
2888
2890
  }
2889
2891
  this.isLegacySync = await isLegacySyncWasm(bufferOrPath);
2890
- this.port = await this.portManager.allocate();
2892
+ if (config?.httpPort !== void 0) {
2893
+ const pinned = config.httpPort;
2894
+ if (!await this.portManager.isPortFree(pinned)) {
2895
+ throw new Error(
2896
+ `fastedge-run port ${pinned} is not available \u2014 release it or choose a different httpPort in fastedge-config.test.json`
2897
+ );
2898
+ }
2899
+ this.port = pinned;
2900
+ this.isPinnedPort = true;
2901
+ } else {
2902
+ this.port = await this.portManager.allocate();
2903
+ this.isPinnedPort = false;
2904
+ }
2891
2905
  const wasi_http = !this.isLegacySync;
2892
2906
  const args = [
2893
2907
  "http",
@@ -2919,7 +2933,13 @@ var HttpWasmRunner = class {
2919
2933
  await this.waitForServerReady(this.port, timeout);
2920
2934
  }
2921
2935
  /**
2922
- * Execute an HTTP request through the WASM module
2936
+ * Execute an HTTP request through the WASM module.
2937
+ *
2938
+ * Redirects are surfaced verbatim — `fetch` is called with
2939
+ * `redirect: "manual"` so 3xx responses (status + `Location`) reach the
2940
+ * caller intact. This matches FastEdge edge behaviour, which returns
2941
+ * redirects to the client rather than following them server-side. See
2942
+ * `IWasmRunner.execute` for the public contract.
2923
2943
  */
2924
2944
  async execute(request) {
2925
2945
  if (!this.port || !this.process) {
@@ -2932,8 +2952,12 @@ var HttpWasmRunner = class {
2932
2952
  method: request.method,
2933
2953
  headers: request.headers,
2934
2954
  body: request.body || void 0,
2935
- signal: AbortSignal.timeout(3e4)
2955
+ signal: AbortSignal.timeout(3e4),
2936
2956
  // 30 second timeout
2957
+ // Surface 3xx responses verbatim so tests can assert on status/Location.
2958
+ // A FastEdge edge returns redirects to the client rather than following
2959
+ // them server-side; production parity requires the same here.
2960
+ redirect: "manual"
2937
2961
  });
2938
2962
  const arrayBuffer = await response.arrayBuffer();
2939
2963
  const bodyBuffer = Buffer.from(arrayBuffer);
@@ -3024,8 +3048,11 @@ var HttpWasmRunner = class {
3024
3048
  this.process = null;
3025
3049
  }
3026
3050
  if (this.port !== null) {
3027
- this.portManager.release(this.port);
3051
+ if (!this.isPinnedPort) {
3052
+ this.portManager.release(this.port);
3053
+ }
3028
3054
  this.port = null;
3055
+ this.isPinnedPort = false;
3029
3056
  }
3030
3057
  if (this.tempWasmPath) {
3031
3058
  await removeTempWasmFile(this.tempWasmPath);
@@ -3242,6 +3269,9 @@ var PortManager = class {
3242
3269
  * This is necessary when multiple server processes run simultaneously —
3243
3270
  * each has its own PortManager with independent in-memory state, so
3244
3271
  * in-memory tracking alone is not enough to prevent cross-process conflicts.
3272
+ *
3273
+ * Public so pinned-port callers (HttpWasmRunner with RunnerConfig.httpPort)
3274
+ * can reuse the same OS-level check without going through allocate().
3245
3275
  */
3246
3276
  isPortFree(port) {
3247
3277
  return new Promise((resolve) => {
package/dist/lib/index.js CHANGED
@@ -2530,7 +2530,7 @@ var ProxyWasmRunner = class {
2530
2530
  }
2531
2531
  }
2532
2532
  createImports() {
2533
- const wasi = new WASI({ version: "preview1" });
2533
+ const wasi = new WASI({ version: "preview1", env: this.dictionary.getAll() });
2534
2534
  const wasiImport = wasi.wasiImport;
2535
2535
  return {
2536
2536
  env: this.hostFunctions.createImports(),
@@ -2817,6 +2817,8 @@ var HttpWasmRunner = class {
2817
2817
  this.stateManager = null;
2818
2818
  this.dotenvEnabled = true;
2819
2819
  this.dotenvPath = null;
2820
+ /** Pinned ports bypass PortManager allocation and must not be released back to it. */
2821
+ this.isPinnedPort = false;
2820
2822
  /** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
2821
2823
  this.isLegacySync = false;
2822
2824
  this.portManager = portManager;
@@ -2843,7 +2845,19 @@ var HttpWasmRunner = class {
2843
2845
  this.tempWasmPath = wasmPath;
2844
2846
  }
2845
2847
  this.isLegacySync = await isLegacySyncWasm(bufferOrPath);
2846
- this.port = await this.portManager.allocate();
2848
+ if (config?.httpPort !== void 0) {
2849
+ const pinned = config.httpPort;
2850
+ if (!await this.portManager.isPortFree(pinned)) {
2851
+ throw new Error(
2852
+ `fastedge-run port ${pinned} is not available \u2014 release it or choose a different httpPort in fastedge-config.test.json`
2853
+ );
2854
+ }
2855
+ this.port = pinned;
2856
+ this.isPinnedPort = true;
2857
+ } else {
2858
+ this.port = await this.portManager.allocate();
2859
+ this.isPinnedPort = false;
2860
+ }
2847
2861
  const wasi_http = !this.isLegacySync;
2848
2862
  const args = [
2849
2863
  "http",
@@ -2875,7 +2889,13 @@ var HttpWasmRunner = class {
2875
2889
  await this.waitForServerReady(this.port, timeout);
2876
2890
  }
2877
2891
  /**
2878
- * Execute an HTTP request through the WASM module
2892
+ * Execute an HTTP request through the WASM module.
2893
+ *
2894
+ * Redirects are surfaced verbatim — `fetch` is called with
2895
+ * `redirect: "manual"` so 3xx responses (status + `Location`) reach the
2896
+ * caller intact. This matches FastEdge edge behaviour, which returns
2897
+ * redirects to the client rather than following them server-side. See
2898
+ * `IWasmRunner.execute` for the public contract.
2879
2899
  */
2880
2900
  async execute(request) {
2881
2901
  if (!this.port || !this.process) {
@@ -2888,8 +2908,12 @@ var HttpWasmRunner = class {
2888
2908
  method: request.method,
2889
2909
  headers: request.headers,
2890
2910
  body: request.body || void 0,
2891
- signal: AbortSignal.timeout(3e4)
2911
+ signal: AbortSignal.timeout(3e4),
2892
2912
  // 30 second timeout
2913
+ // Surface 3xx responses verbatim so tests can assert on status/Location.
2914
+ // A FastEdge edge returns redirects to the client rather than following
2915
+ // them server-side; production parity requires the same here.
2916
+ redirect: "manual"
2893
2917
  });
2894
2918
  const arrayBuffer = await response.arrayBuffer();
2895
2919
  const bodyBuffer = Buffer.from(arrayBuffer);
@@ -2980,8 +3004,11 @@ var HttpWasmRunner = class {
2980
3004
  this.process = null;
2981
3005
  }
2982
3006
  if (this.port !== null) {
2983
- this.portManager.release(this.port);
3007
+ if (!this.isPinnedPort) {
3008
+ this.portManager.release(this.port);
3009
+ }
2984
3010
  this.port = null;
3011
+ this.isPinnedPort = false;
2985
3012
  }
2986
3013
  if (this.tempWasmPath) {
2987
3014
  await removeTempWasmFile(this.tempWasmPath);
@@ -3198,6 +3225,9 @@ var PortManager = class {
3198
3225
  * This is necessary when multiple server processes run simultaneously —
3199
3226
  * each has its own PortManager with independent in-memory state, so
3200
3227
  * in-memory tracking alone is not enough to prevent cross-process conflicts.
3228
+ *
3229
+ * Public so pinned-port callers (HttpWasmRunner with RunnerConfig.httpPort)
3230
+ * can reuse the same OS-level check without going through allocate().
3201
3231
  */
3202
3232
  isPortFree(port) {
3203
3233
  return new Promise((resolve) => {
@@ -24,6 +24,8 @@ export declare class HttpWasmRunner implements IWasmRunner {
24
24
  private portManager;
25
25
  private dotenvEnabled;
26
26
  private dotenvPath;
27
+ /** Pinned ports bypass PortManager allocation and must not be released back to it. */
28
+ private isPinnedPort;
27
29
  /** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
28
30
  private isLegacySync;
29
31
  constructor(portManager: PortManager, dotenvEnabled?: boolean);
@@ -32,7 +34,13 @@ export declare class HttpWasmRunner implements IWasmRunner {
32
34
  */
33
35
  load(bufferOrPath: Buffer | string, config?: RunnerConfig): Promise<void>;
34
36
  /**
35
- * Execute an HTTP request through the WASM module
37
+ * Execute an HTTP request through the WASM module.
38
+ *
39
+ * Redirects are surfaced verbatim — `fetch` is called with
40
+ * `redirect: "manual"` so 3xx responses (status + `Location`) reach the
41
+ * caller intact. This matches FastEdge edge behaviour, which returns
42
+ * redirects to the client rather than following them server-side. See
43
+ * `IWasmRunner.execute` for the public contract.
36
44
  */
37
45
  execute(request: HttpRequest): Promise<HttpResponse>;
38
46
  /**
@@ -18,6 +18,15 @@ export interface RunnerConfig {
18
18
  enforceProductionPropertyRules?: boolean;
19
19
  /** Override automatic WASM type detection. Use when detection produces wrong results. */
20
20
  runnerType?: WasmType;
21
+ /**
22
+ * HTTP-WASM only. Pin the spawned `fastedge-run` HTTP server to a specific
23
+ * port instead of allocating from the dynamic pool (8100-8199). Intended for
24
+ * Codespaces/Docker port-forwarding setups, stable live-preview URLs, or any
25
+ * external tooling that requires a fixed target. `load()` throws if the port
26
+ * is already in use — there is no fallback to dynamic allocation. Ignored
27
+ * for proxy-wasm runners.
28
+ */
29
+ httpPort?: number;
21
30
  }
22
31
  /**
23
32
  * HTTP Request type for HTTP WASM runner
@@ -54,7 +63,22 @@ export interface IWasmRunner {
54
63
  */
55
64
  load(bufferOrPath: Buffer | string, config?: RunnerConfig): Promise<void>;
56
65
  /**
57
- * Execute a request through the WASM module (HTTP WASM only)
66
+ * Execute a request through the WASM module (HTTP WASM only).
67
+ *
68
+ * Redirects are surfaced verbatim — the underlying fetch uses
69
+ * `redirect: "manual"` so tests can assert on 3xx status and the `Location`
70
+ * header. This matches how a FastEdge edge deployment returns redirects to
71
+ * the client rather than following them server-side.
72
+ *
73
+ * `execute` only hits the WASM app under test — `request.path` is a path on
74
+ * the spawned `fastedge-run` server, not a full URL. To follow a redirect
75
+ * the caller must inspect `response.headers.location`:
76
+ * - Relative Location (`/foo`) — reuse as `request.path` directly.
77
+ * - Absolute same-host Location — extract `pathname + search` via `new URL()`
78
+ * and re-issue with that path.
79
+ * - Absolute cross-host Location — cannot be followed through the runner;
80
+ * the 302 is the terminal state for the test.
81
+ *
58
82
  * @param request The HTTP request to execute
59
83
  * @returns The HTTP response
60
84
  */
@@ -18,8 +18,11 @@ export declare class PortManager {
18
18
  * This is necessary when multiple server processes run simultaneously —
19
19
  * each has its own PortManager with independent in-memory state, so
20
20
  * in-memory tracking alone is not enough to prevent cross-process conflicts.
21
+ *
22
+ * Public so pinned-port callers (HttpWasmRunner with RunnerConfig.httpPort)
23
+ * can reuse the same OS-level check without going through allocate().
21
24
  */
22
- private isPortFree;
25
+ isPortFree(port: number): Promise<boolean>;
23
26
  /**
24
27
  * Allocate an available port from the pool.
25
28
  * Combines in-memory tracking (avoids TCP TIME_WAIT reuse within this process)
@@ -6,6 +6,7 @@ export declare const ApiLoadBodySchema: z.ZodObject<{
6
6
  enabled: z.ZodOptional<z.ZodBoolean>;
7
7
  path: z.ZodOptional<z.ZodString>;
8
8
  }, z.core.$strip>>;
9
+ httpPort: z.ZodOptional<z.ZodNumber>;
9
10
  }, z.core.$strip>;
10
11
  export declare const ApiSendBodySchema: z.ZodObject<{
11
12
  url: z.ZodUnion<readonly [z.ZodLiteral<"built-in">, z.ZodString]>;
@@ -52,6 +53,7 @@ export declare const ApiConfigBodySchema: z.ZodObject<{
52
53
  path: z.ZodOptional<z.ZodString>;
53
54
  }, z.core.$strip>>;
54
55
  appType: z.ZodLiteral<"http-wasm">;
56
+ httpPort: z.ZodOptional<z.ZodNumber>;
55
57
  request: z.ZodObject<{
56
58
  method: z.ZodDefault<z.ZodString>;
57
59
  path: z.ZodString;
@@ -56,6 +56,7 @@ declare const HttpConfigSchema: z.ZodObject<{
56
56
  path: z.ZodOptional<z.ZodString>;
57
57
  }, z.core.$strip>>;
58
58
  appType: z.ZodLiteral<"http-wasm">;
59
+ httpPort: z.ZodOptional<z.ZodNumber>;
59
60
  request: z.ZodObject<{
60
61
  method: z.ZodDefault<z.ZodString>;
61
62
  path: z.ZodString;
@@ -76,6 +77,7 @@ export declare const TestConfigSchema: z.ZodUnion<readonly [z.ZodObject<{
76
77
  path: z.ZodOptional<z.ZodString>;
77
78
  }, z.core.$strip>>;
78
79
  appType: z.ZodLiteral<"http-wasm">;
80
+ httpPort: z.ZodOptional<z.ZodNumber>;
79
81
  request: z.ZodObject<{
80
82
  method: z.ZodDefault<z.ZodString>;
81
83
  path: z.ZodString;
@@ -2601,7 +2601,7 @@ var ProxyWasmRunner = class {
2601
2601
  }
2602
2602
  }
2603
2603
  createImports() {
2604
- const wasi = new import_node_wasi.WASI({ version: "preview1" });
2604
+ const wasi = new import_node_wasi.WASI({ version: "preview1", env: this.dictionary.getAll() });
2605
2605
  const wasiImport = wasi.wasiImport;
2606
2606
  return {
2607
2607
  env: this.hostFunctions.createImports(),
@@ -2888,6 +2888,8 @@ var HttpWasmRunner = class {
2888
2888
  this.stateManager = null;
2889
2889
  this.dotenvEnabled = true;
2890
2890
  this.dotenvPath = null;
2891
+ /** Pinned ports bypass PortManager allocation and must not be released back to it. */
2892
+ this.isPinnedPort = false;
2891
2893
  /** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
2892
2894
  this.isLegacySync = false;
2893
2895
  this.portManager = portManager;
@@ -2914,7 +2916,19 @@ var HttpWasmRunner = class {
2914
2916
  this.tempWasmPath = wasmPath;
2915
2917
  }
2916
2918
  this.isLegacySync = await isLegacySyncWasm(bufferOrPath);
2917
- this.port = await this.portManager.allocate();
2919
+ if (config?.httpPort !== void 0) {
2920
+ const pinned = config.httpPort;
2921
+ if (!await this.portManager.isPortFree(pinned)) {
2922
+ throw new Error(
2923
+ `fastedge-run port ${pinned} is not available \u2014 release it or choose a different httpPort in fastedge-config.test.json`
2924
+ );
2925
+ }
2926
+ this.port = pinned;
2927
+ this.isPinnedPort = true;
2928
+ } else {
2929
+ this.port = await this.portManager.allocate();
2930
+ this.isPinnedPort = false;
2931
+ }
2918
2932
  const wasi_http = !this.isLegacySync;
2919
2933
  const args = [
2920
2934
  "http",
@@ -2946,7 +2960,13 @@ var HttpWasmRunner = class {
2946
2960
  await this.waitForServerReady(this.port, timeout);
2947
2961
  }
2948
2962
  /**
2949
- * Execute an HTTP request through the WASM module
2963
+ * Execute an HTTP request through the WASM module.
2964
+ *
2965
+ * Redirects are surfaced verbatim — `fetch` is called with
2966
+ * `redirect: "manual"` so 3xx responses (status + `Location`) reach the
2967
+ * caller intact. This matches FastEdge edge behaviour, which returns
2968
+ * redirects to the client rather than following them server-side. See
2969
+ * `IWasmRunner.execute` for the public contract.
2950
2970
  */
2951
2971
  async execute(request) {
2952
2972
  if (!this.port || !this.process) {
@@ -2959,8 +2979,12 @@ var HttpWasmRunner = class {
2959
2979
  method: request.method,
2960
2980
  headers: request.headers,
2961
2981
  body: request.body || void 0,
2962
- signal: AbortSignal.timeout(3e4)
2982
+ signal: AbortSignal.timeout(3e4),
2963
2983
  // 30 second timeout
2984
+ // Surface 3xx responses verbatim so tests can assert on status/Location.
2985
+ // A FastEdge edge returns redirects to the client rather than following
2986
+ // them server-side; production parity requires the same here.
2987
+ redirect: "manual"
2964
2988
  });
2965
2989
  const arrayBuffer = await response.arrayBuffer();
2966
2990
  const bodyBuffer = Buffer.from(arrayBuffer);
@@ -3051,8 +3075,11 @@ var HttpWasmRunner = class {
3051
3075
  this.process = null;
3052
3076
  }
3053
3077
  if (this.port !== null) {
3054
- this.portManager.release(this.port);
3078
+ if (!this.isPinnedPort) {
3079
+ this.portManager.release(this.port);
3080
+ }
3055
3081
  this.port = null;
3082
+ this.isPinnedPort = false;
3056
3083
  }
3057
3084
  if (this.tempWasmPath) {
3058
3085
  await removeTempWasmFile(this.tempWasmPath);
@@ -3269,6 +3296,9 @@ var PortManager = class {
3269
3296
  * This is necessary when multiple server processes run simultaneously —
3270
3297
  * each has its own PortManager with independent in-memory state, so
3271
3298
  * in-memory tracking alone is not enough to prevent cross-process conflicts.
3299
+ *
3300
+ * Public so pinned-port callers (HttpWasmRunner with RunnerConfig.httpPort)
3301
+ * can reuse the same OS-level check without going through allocate().
3272
3302
  */
3273
3303
  isPortFree(port) {
3274
3304
  return new Promise((resolve) => {
@@ -3412,6 +3442,13 @@ var CdnConfigSchema = BaseConfigSchema.extend({
3412
3442
  });
3413
3443
  var HttpConfigSchema = BaseConfigSchema.extend({
3414
3444
  appType: import_zod.z.literal("http-wasm"),
3445
+ /**
3446
+ * Pin the fastedge-run subprocess to a specific port instead of allocating
3447
+ * from the dynamic pool (8100-8199). Use for Codespaces/Docker port-forwarding,
3448
+ * stable live-preview URLs, or tooling that needs a fixed target. Load fails
3449
+ * fast if the port is already in use.
3450
+ */
3451
+ httpPort: import_zod.z.number().int().min(1024).max(65535).optional(),
3415
3452
  request: HttpRequestConfigSchema
3416
3453
  });
3417
3454
  var TestConfigSchema = import_zod.z.union([HttpConfigSchema, CdnConfigSchema]);
@@ -2537,7 +2537,7 @@ var ProxyWasmRunner = class {
2537
2537
  }
2538
2538
  }
2539
2539
  createImports() {
2540
- const wasi = new WASI({ version: "preview1" });
2540
+ const wasi = new WASI({ version: "preview1", env: this.dictionary.getAll() });
2541
2541
  const wasiImport = wasi.wasiImport;
2542
2542
  return {
2543
2543
  env: this.hostFunctions.createImports(),
@@ -2824,6 +2824,8 @@ var HttpWasmRunner = class {
2824
2824
  this.stateManager = null;
2825
2825
  this.dotenvEnabled = true;
2826
2826
  this.dotenvPath = null;
2827
+ /** Pinned ports bypass PortManager allocation and must not be released back to it. */
2828
+ this.isPinnedPort = false;
2827
2829
  /** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
2828
2830
  this.isLegacySync = false;
2829
2831
  this.portManager = portManager;
@@ -2850,7 +2852,19 @@ var HttpWasmRunner = class {
2850
2852
  this.tempWasmPath = wasmPath;
2851
2853
  }
2852
2854
  this.isLegacySync = await isLegacySyncWasm(bufferOrPath);
2853
- this.port = await this.portManager.allocate();
2855
+ if (config?.httpPort !== void 0) {
2856
+ const pinned = config.httpPort;
2857
+ if (!await this.portManager.isPortFree(pinned)) {
2858
+ throw new Error(
2859
+ `fastedge-run port ${pinned} is not available \u2014 release it or choose a different httpPort in fastedge-config.test.json`
2860
+ );
2861
+ }
2862
+ this.port = pinned;
2863
+ this.isPinnedPort = true;
2864
+ } else {
2865
+ this.port = await this.portManager.allocate();
2866
+ this.isPinnedPort = false;
2867
+ }
2854
2868
  const wasi_http = !this.isLegacySync;
2855
2869
  const args = [
2856
2870
  "http",
@@ -2882,7 +2896,13 @@ var HttpWasmRunner = class {
2882
2896
  await this.waitForServerReady(this.port, timeout);
2883
2897
  }
2884
2898
  /**
2885
- * Execute an HTTP request through the WASM module
2899
+ * Execute an HTTP request through the WASM module.
2900
+ *
2901
+ * Redirects are surfaced verbatim — `fetch` is called with
2902
+ * `redirect: "manual"` so 3xx responses (status + `Location`) reach the
2903
+ * caller intact. This matches FastEdge edge behaviour, which returns
2904
+ * redirects to the client rather than following them server-side. See
2905
+ * `IWasmRunner.execute` for the public contract.
2886
2906
  */
2887
2907
  async execute(request) {
2888
2908
  if (!this.port || !this.process) {
@@ -2895,8 +2915,12 @@ var HttpWasmRunner = class {
2895
2915
  method: request.method,
2896
2916
  headers: request.headers,
2897
2917
  body: request.body || void 0,
2898
- signal: AbortSignal.timeout(3e4)
2918
+ signal: AbortSignal.timeout(3e4),
2899
2919
  // 30 second timeout
2920
+ // Surface 3xx responses verbatim so tests can assert on status/Location.
2921
+ // A FastEdge edge returns redirects to the client rather than following
2922
+ // them server-side; production parity requires the same here.
2923
+ redirect: "manual"
2900
2924
  });
2901
2925
  const arrayBuffer = await response.arrayBuffer();
2902
2926
  const bodyBuffer = Buffer.from(arrayBuffer);
@@ -2987,8 +3011,11 @@ var HttpWasmRunner = class {
2987
3011
  this.process = null;
2988
3012
  }
2989
3013
  if (this.port !== null) {
2990
- this.portManager.release(this.port);
3014
+ if (!this.isPinnedPort) {
3015
+ this.portManager.release(this.port);
3016
+ }
2991
3017
  this.port = null;
3018
+ this.isPinnedPort = false;
2992
3019
  }
2993
3020
  if (this.tempWasmPath) {
2994
3021
  await removeTempWasmFile(this.tempWasmPath);
@@ -3205,6 +3232,9 @@ var PortManager = class {
3205
3232
  * This is necessary when multiple server processes run simultaneously —
3206
3233
  * each has its own PortManager with independent in-memory state, so
3207
3234
  * in-memory tracking alone is not enough to prevent cross-process conflicts.
3235
+ *
3236
+ * Public so pinned-port callers (HttpWasmRunner with RunnerConfig.httpPort)
3237
+ * can reuse the same OS-level check without going through allocate().
3208
3238
  */
3209
3239
  isPortFree(port) {
3210
3240
  return new Promise((resolve) => {
@@ -3348,6 +3378,13 @@ var CdnConfigSchema = BaseConfigSchema.extend({
3348
3378
  });
3349
3379
  var HttpConfigSchema = BaseConfigSchema.extend({
3350
3380
  appType: z.literal("http-wasm"),
3381
+ /**
3382
+ * Pin the fastedge-run subprocess to a specific port instead of allocating
3383
+ * from the dynamic pool (8100-8199). Use for Codespaces/Docker port-forwarding,
3384
+ * stable live-preview URLs, or tooling that needs a fixed target. Load fails
3385
+ * fast if the port is already in use.
3386
+ */
3387
+ httpPort: z.number().int().min(1024).max(65535).optional(),
3351
3388
  request: HttpRequestConfigSchema
3352
3389
  });
3353
3390
  var TestConfigSchema = z.union([HttpConfigSchema, CdnConfigSchema]);
@@ -36,6 +36,22 @@ export declare function runFlow(runner: IWasmRunner, options: FlowOptions): Prom
36
36
  * - method defaults to "GET"
37
37
  * - headers defaults to {}
38
38
  * - body defaults to ""
39
+ *
40
+ * Redirects are surfaced verbatim — a 302 from the WASM is returned to the
41
+ * caller with its `Location` header preserved, matching FastEdge edge
42
+ * behaviour.
43
+ *
44
+ * `runHttpRequest` targets the WASM app under test only (`options.path` is a
45
+ * path on the local `fastedge-run` server, not a full URL). Following a
46
+ * redirect therefore depends on the shape of `response.headers.location`:
47
+ *
48
+ * - Relative (e.g. `/auth/complete`) — pass it directly as `path` in a second
49
+ * `runHttpRequest` call.
50
+ * - Absolute with the app's own host — parse with `new URL(...)`, then
51
+ * re-issue against `url.pathname + url.search`.
52
+ * - Absolute with an external host — cannot be followed through the runner;
53
+ * that redirect is the end of the test, assert on status + Location and
54
+ * stop there.
39
55
  */
40
56
  export declare function runHttpRequest(runner: IWasmRunner, options: HttpRequestOptions): Promise<HttpResponse>;
41
57
  /**