Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions extensions/ext-yaml/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
/**
* The tags a JSON-representable document may carry explicitly. `yaml`'s
* `Schema.knownTags` fallback resolves YAML 1.1 tags such as `!!binary`,
* `!!timestamp`, `!!set` and `!!omap` even under the 1.2 core schema, and does
* without raising a warning, so the parser options alone cannot express
* `!!timestamp`, `!!set` and `!!omap` even under the 1.2 core schema without
* raising a warning, so the parser options alone cannot express
* `@std/yaml`'s JSON schema. Rejecting every other explicit tag does.
*/
const JSON_SCHEMA_TAGS: ReadonlySet<string> = new Set([
Expand Down
4 changes: 4 additions & 0 deletions src/config/declarative-evaluator-worker-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type DeclarativeConfigWorkerInfrastructureReason =
| "worker-aborted"
| "worker-overloaded"
| "worker-protocol"
| "worker-memory-limit-unavailable"
| "worker-timeout"
| "worker-unavailable";

Expand Down Expand Up @@ -175,6 +176,7 @@ const ERROR_REASON_TABLE = ObjectFreeze(
"worker-aborted": true,
"worker-overloaded": true,
"worker-protocol": true,
"worker-memory-limit-unavailable": true,
"worker-timeout": true,
"worker-unavailable": true,
} as const satisfies Readonly<Record<DeclarativeConfigErrorReason, true>>,
Expand Down Expand Up @@ -347,6 +349,7 @@ function isWorkerReason(
return value === "worker-aborted" ||
value === "worker-overloaded" ||
value === "worker-protocol" ||
value === "worker-memory-limit-unavailable" ||
value === "worker-timeout" ||
value === "worker-unavailable";
}
Expand Down Expand Up @@ -378,6 +381,7 @@ function isLegalErrorTuple(
if (phase !== "worker" || !isWorkerReason(reason)) return false;
return retryable === (
reason === "worker-overloaded" ||
reason === "worker-memory-limit-unavailable" ||
reason === "worker-timeout" ||
reason === "worker-unavailable"
);
Expand Down
30 changes: 29 additions & 1 deletion src/config/declarative-evaluator-worker-runner.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
import { assertEquals } from "#veryfront/testing/assert.ts";
import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { isBun } from "#veryfront/platform/compat/runtime.ts";
import {
createPreparedDeclarativeConfigWorkerPayload,
prepareDeclarativeConfigContext,
} from "./declarative-evaluator.ts";
import { evaluatePreparedDeclarativeConfigInWorker } from "./declarative-evaluator-worker-runner.ts";

describe("declarative config runtime worker", () => {
it("rejects Bun when bounded worker memory limits are unavailable", async () => {
if (!isBun) return;
const context = await prepareDeclarativeConfigContext({
environmentName: "preview",
environment: {},
});
const payload = createPreparedDeclarativeConfigWorkerPayload(
`export default { title: "unreachable" };`,
context,
"veryfront.config.ts",
);

const error = await assertRejects(() => evaluatePreparedDeclarativeConfigInWorker(payload));

assertEquals(
error instanceof Error && "reason" in error
? (error as { reason?: unknown }).reason
: undefined,
"worker-memory-limit-unavailable",
);
assertEquals(
error instanceof Error ? error.message : undefined,
"Hosted configuration rejected (evaluator-unavailable: worker-memory-limit-unavailable)",
);
});

it("evaluates a hosted TypeScript config", async () => {
if (isBun) return;
const context = await prepareDeclarativeConfigContext({
environmentName: "preview",
environment: { TENANT: "tenant-value" },
Expand Down
22 changes: 18 additions & 4 deletions src/config/declarative-evaluator-worker-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
*/

import { isBun, isDeno, isNode } from "#veryfront/platform/compat/runtime.ts";
import type { PreparedDeclarativeConfigWorkerPayload } from "./declarative-evaluator.ts";
import {
DeclarativeConfigEvaluationError,
type PreparedDeclarativeConfigWorkerPayload,
} from "./declarative-evaluator.ts";
import type { ConfigSnapshotRecord } from "./snapshot.ts";
import {
createDeclarativeConfigWorkerInfrastructureError,
Expand Down Expand Up @@ -529,7 +532,11 @@ async function createRuntimeWorkerEndpoint(): Promise<
DeclarativeConfigWorkerEndpoint
> {
if (isDeno) return createDenoWorkerEndpoint();
if (isBun) return await createNodeWorkerEndpoint();
if (isBun) {
throw createDeclarativeConfigWorkerInfrastructureError(
"worker-memory-limit-unavailable",
);
}
if (isNode) return await createNodeWorkerEndpoint();
throw createDeclarativeConfigWorkerInfrastructureError("worker-unavailable");
}
Expand Down Expand Up @@ -664,9 +671,16 @@ function beginEvaluationWithEndpointFactory(
let createdEndpoint: DeclarativeConfigWorkerEndpoint;
try {
createdEndpoint = await endpointFactory();
} catch {
} catch (error) {
drainStartupLifecycle();
rejectInfrastructure("worker-unavailable");
if (
error instanceof DeclarativeConfigEvaluationError &&
error.phase === "worker"
) {
settle({ kind: "reject", error });
} else {
rejectInfrastructure("worker-unavailable");
}
drainLifecycleIfComplete();
return;
}
Expand Down
1 change: 1 addition & 0 deletions src/config/declarative-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export type DeclarativeConfigErrorReason =
| "worker-aborted"
| "worker-overloaded"
| "worker-protocol"
| "worker-memory-limit-unavailable"
| "worker-timeout"
| "worker-unavailable";

Expand Down
38 changes: 38 additions & 0 deletions src/extensions/first-party-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,44 @@ describe("first-party extension imports", () => {
assertEquals(isMissingFirstPartyExtensionModule(unrelated), false);
});

it("parses Bun missing-module reports from strings and object-shaped errors", () => {
const relative = {
message:
"ResolveMessage: Cannot find module './parser-only' from '/app/extensions/ext-parser-babel/src/index.ts'",
};
assertEquals(
isMissingFirstPartyExtensionModule(relative, [
"extensions/ext-parser-babel/src/parser-only",
]),
true,
);
assertEquals(
isMissingFirstPartyExtensionModule(relative, [
"extensions/ext-parser-babel/src/other",
]),
false,
);

const packageSpecifier = {
message:
"Cannot find module '@veryfront/ext-parser-babel/parser-only' from '/app/loader.js'",
};
assertEquals(
isMissingFirstPartyExtensionModule(packageSpecifier, [
"@veryfront/ext-parser-babel/parser-only",
]),
true,
);

assertEquals(
isMissingFirstPartyExtensionModule(
"Cannot find module '@veryfront/ext-parser-babel' from '/app/loader.js'",
["@veryfront/ext-parser-babel"],
),
true,
);
});

it("requires a full recognized message when no stable code is present", () => {
assertEquals(
isMissingFirstPartyExtensionModule(
Expand Down
3 changes: 2 additions & 1 deletion src/modules/server/classify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,15 @@ describe("classifyModuleRequest", () => {
}
});

it("normalizes lowercase and uppercase encoded caret operators", () => {
it("normalizes encoded caret version operators before the source marker", () => {
for (const encodedCaret of ["%5e", "%5E"]) {
const result = classifyModuleRequest(
url(`/_vf_modules/_cross/demo@${encodedCaret}1.0.0/@/lib/utils.js`),
);
assertEquals(result.kind, "cross-project-versioned");
if (result.kind === "cross-project-versioned") {
assertEquals(result.version, "^1.0.0");
assertEquals(result.path, "lib/utils.js");
}
}
});
Expand Down
86 changes: 86 additions & 0 deletions src/platform/adapters/runtime/node/http-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,92 @@ function createDeferred<T>(): {
}

describe("NodeServer lifecycle", () => {
it("accepts the lowest and highest valid listener ports", async () => {
if (!isNode) return;
const listenDescriptor = Object.getOwnPropertyDescriptor(
NativeHttpServer.prototype,
"listen",
);
const addressDescriptor = Object.getOwnPropertyDescriptor(
NativeHttpServer.prototype,
"address",
);
const closeDescriptor = Object.getOwnPropertyDescriptor(
NativeHttpServer.prototype,
"close",
);
const originalListen = NativeHttpServer.prototype.listen;
const originalAddress = NativeHttpServer.prototype.address;
const originalClose = NativeHttpServer.prototype.close;
const listenedPorts: number[] = [];
let currentPort = 0;

NativeHttpServer.prototype.listen = function (
this: NativeHttpServer,
port?: number,
): NativeHttpServer {
currentPort = port ?? 0;
listenedPorts.push(currentPort);
queueMicrotask(() => this.emit("listening"));
return this;
} as typeof originalListen;
NativeHttpServer.prototype.address = function () {
return { address: "127.0.0.1", family: "IPv4", port: currentPort };
} as typeof originalAddress;
NativeHttpServer.prototype.close = function (
this: NativeHttpServer,
callback?: (error?: Error) => void,
): NativeHttpServer {
queueMicrotask(() => {
this.emit("close");
callback?.();
});
return this;
} as typeof originalClose;

try {
for (const port of [0, 65_535]) {
const server = await createNodeServer(() => new Response("ok"), {
hostname: "127.0.0.1",
port,
});
assertEquals(server.addr.port, port);
await server.stop();
}
} finally {
if (listenDescriptor) {
Object.defineProperty(NativeHttpServer.prototype, "listen", listenDescriptor);
} else {
Reflect.deleteProperty(NativeHttpServer.prototype, "listen");
}
if (addressDescriptor) {
Object.defineProperty(NativeHttpServer.prototype, "address", addressDescriptor);
} else {
Reflect.deleteProperty(NativeHttpServer.prototype, "address");
}
if (closeDescriptor) {
Object.defineProperty(NativeHttpServer.prototype, "close", closeDescriptor);
} else {
Reflect.deleteProperty(NativeHttpServer.prototype, "close");
}
}
assertEquals(listenedPorts, [0, 65_535]);
});

it("rejects invalid listener ports with the exact validation message", async () => {
for (const port of [-1, 65_536, 1.5]) {
await assertRejects(
() =>
createNodeServer(() => new Response("unreachable"), {
hostname: "127.0.0.1",
port,
}),
RangeError,
`Node server port must be an integer from 0 to 65535, got ${port}`,
);
}
});

it("shares shutdown and retries only the failed HTTP close phase", async () => {
let upgradeDisposeCalls = 0;
let closeCalls = 0;
Expand Down
29 changes: 29 additions & 0 deletions src/proxy/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,35 @@ describe("shouldRetryUpstreamRequest", () => {
});

describe("getReplayableRequestBodies", () => {
async function readBodyBytes(body: ReadableStream<Uint8Array> | null): Promise<number[]> {
const bytes = await new Response(body).arrayBuffer();
return [...new Uint8Array(bytes)];
}

it("replays a multi-chunk body sequentially across retries", async () => {
const encoder = new TextEncoder();
const chunks = ["alpha", ":", "beta"].map((chunk) => encoder.encode(chunk));
const expected = chunks.flatMap((chunk) => [...chunk]);
const body = new ReadableStream<Uint8Array>({
start(controller) {
for (const chunk of chunks) controller.enqueue(chunk);
controller.close();
},
});
const request = {
method: "POST",
headers: new Headers({ "content-length": "10" }),
body,
} as Request;

const bodies = getReplayableRequestBodies(request, 3);

assertEquals(bodies.length, 4);
for (const replay of bodies) {
assertEquals(await readBodyBytes(replay), expected);
}
});

it("creates an independent signed payload stream for every attempt", async () => {
const payload = JSON.stringify({ run: { runId: "run_1" } });
const request = new Request(RUN_STREAM_URL, {
Expand Down
Loading