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
217 changes: 206 additions & 11 deletions scripts/dev-runner.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as NodeOS from "node:os";
import * as NetService from "@t3tools/shared/Net";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { assert, describe, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import { ChildProcessSpawner } from "effect/unstable/process";

import {
checkPortAvailabilityOnHosts,
Expand All @@ -11,8 +19,49 @@ import {
getDevRunnerModeArgs,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
} from "./dev-runner.ts";

const emptyConfigLayer = ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }));
const netServiceLayer = Layer.succeed(NetService.NetService, {
canListenOnHost: () => Effect.succeed(true),
isPortAvailableOnLoopback: () => Effect.succeed(true),
reserveLoopbackPort: () => Effect.succeed(49_152),
findAvailablePort: (port) => Effect.succeed(port),
});

function mockProcess(exit: number | PlatformError.PlatformError) {
return ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(1),
exitCode:
typeof exit === "number"
? Effect.succeed(ChildProcessSpawner.ExitCode(exit))
: Effect.fail(exit),
isRunning: Effect.succeed(false),
kill: () => Effect.void,
unref: Effect.succeed(Effect.void),
stdin: Sink.drain,
stdout: Stream.empty,
stderr: Stream.empty,
all: Stream.empty,
getInputFd: () => Sink.drain,
getOutputFd: () => Stream.empty,
});
}

const devServerInput = {
mode: "dev:server",
t3Home: "/tmp/t3code-dev-runner",
noBrowser: undefined,
autoBootstrapProjectFromCwd: undefined,
logWebSocketEvents: undefined,
host: undefined,
port: 13_773,
devUrl: undefined,
dryRun: false,
runArgs: ["--inspect", "secret-token-value"],
} as const;

it.layer(NodeServices.layer)("dev-runner", (it) => {
describe("getDevRunnerModeArgs", () => {
it.effect("lets Vite+ honor the desktop dev task graph", () =>
Expand Down Expand Up @@ -42,8 +91,8 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {

describe("resolveOffset", () => {
it.effect("uses explicit T3CODE_PORT_OFFSET when provided", () =>
Effect.sync(() => {
const result = resolveOffset({ portOffset: 12, devInstance: undefined });
Effect.gen(function* () {
const result = yield* resolveOffset({ portOffset: 12, devInstance: undefined });
assert.deepStrictEqual(result, {
offset: 12,
source: "T3CODE_PORT_OFFSET=12",
Expand All @@ -52,23 +101,27 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);

it.effect("hashes non-numeric instance values", () =>
Effect.sync(() => {
const result = resolveOffset({ portOffset: undefined, devInstance: "feature-branch" });
Effect.gen(function* () {
const result = yield* resolveOffset({
portOffset: undefined,
devInstance: "feature-branch",
});
assert.ok(result.offset >= 1);
assert.ok(result.offset <= 3000);
}),
);

it.effect("throws for negative port offset", () =>
it.effect("returns structured context for a negative port offset", () =>
Effect.gen(function* () {
const error = yield* Effect.flip(
Effect.try({
try: () => resolveOffset({ portOffset: -1, devInstance: undefined }),
catch: (cause) => String(cause),
}),
const error = yield* resolveOffset({ portOffset: -1, devInstance: undefined }).pipe(
Effect.flip,
);

assert.ok(error.includes("Invalid T3CODE_PORT_OFFSET"));
assert.equal(error._tag, "DevRunnerInvalidPortOffsetError");
assert.equal(error.configKey, "T3CODE_PORT_OFFSET");
assert.equal(error.portOffset, -1);
assert.equal(error.minimum, 0);
assert.ok(!("cause" in error));
}),
);
});
Expand Down Expand Up @@ -289,6 +342,28 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
assert.equal(offset, 59_802);
}),
);

it.effect("reports the exhausted range and required port set", () =>
Effect.gen(function* () {
const error = yield* findFirstAvailableOffset({
startOffset: 51_763,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
}).pipe(Effect.flip);

if (error._tag !== "DevRunnerPortExhaustedError") {
assert.fail(`Unexpected error: ${error._tag}`);
}
assert.equal(error.startOffset, 51_763);
assert.equal(error.requireServerPort, true);
assert.equal(error.requireWebPort, false);
assert.equal(error.baseServerPort, 13_773);
assert.equal(error.baseWebPort, 5_733);
assert.equal(error.maximumPort, 65_535);
assert.ok(!("cause" in error));
}),
);
});

describe("checkPortAvailabilityOnHosts", () => {
Expand Down Expand Up @@ -395,4 +470,124 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);
});

describe("runDevRunnerWithInput", () => {
it.effect("preserves invalid configuration as the exact cause", () =>
Effect.gen(function* () {
const error = yield* runDevRunnerWithInput({ ...devServerInput, dryRun: true }).pipe(
Effect.provide(
Layer.merge(
netServiceLayer,
ConfigProvider.layer(
ConfigProvider.fromEnv({ env: { T3CODE_PORT_OFFSET: "not-an-integer" } }),
),
),
),
Effect.flip,
);

if (error._tag !== "DevRunnerConfigurationError") {
assert.fail(`Unexpected error: ${error._tag}`);
}
assert.deepStrictEqual(error.configKeys, ["T3CODE_PORT_OFFSET", "T3CODE_DEV_INSTANCE"]);
assert.ok(error.cause !== undefined);
assert.ok(!error.message.includes(String((error.cause as Error).message)));
}),
);

it.effect("preserves process spawn context and the exact platform cause", () => {
const cause = PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
description: "vp was not found",
});
const spawnerLayer = Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() => Effect.fail(cause)),
);

return Effect.gen(function* () {
const error = yield* runDevRunnerWithInput(devServerInput).pipe(
Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)),
Effect.provideService(HostProcessPlatform, "linux"),
Effect.flip,
);

if (error._tag !== "DevRunnerProcessError") {
assert.fail(`Unexpected error: ${error._tag}`);
}
assert.equal(error.operation, "spawn");
assert.equal(error.mode, "dev:server");
assert.equal(error.executable, "vp");
assert.equal(error.argumentCount, 5);
assert.equal(error.shell, false);
assert.equal(error.cause, cause);
assert.ok(!error.message.includes(cause.message));
assert.notProperty(error, "args");
assert.notInclude(error.message, "secret-token-value");
});
});

it.effect("reports non-zero exits without manufacturing a cause", () => {
const spawnerLayer = Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() => Effect.succeed(mockProcess(17))),
);

return Effect.gen(function* () {
const error = yield* runDevRunnerWithInput(devServerInput).pipe(
Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)),
Effect.provideService(HostProcessPlatform, "linux"),
Effect.flip,
);

if (error._tag !== "DevRunnerProcessExitError") {
assert.fail(`Unexpected error: ${error._tag}`);
}
assert.equal(error.mode, "dev:server");
assert.equal(error.executable, "vp");
assert.equal(error.argumentCount, 5);
assert.equal(error.shell, false);
assert.equal(error.exitCode, 17);
assert.ok(!("cause" in error));
assert.notProperty(error, "args");
assert.notInclude(error.message, "secret-token-value");
});
});

it.effect("preserves wait-for-exit failures as the exact cause", () => {
const cause = PlatformError.systemError({
_tag: "Unknown",
module: "ChildProcess",
method: "exitCode",
description: "process status became unavailable",
});
const spawnerLayer = Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() => Effect.succeed(mockProcess(cause))),
);

return Effect.gen(function* () {
const error = yield* runDevRunnerWithInput(devServerInput).pipe(
Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)),
Effect.provideService(HostProcessPlatform, "linux"),
Effect.flip,
);

if (error._tag !== "DevRunnerProcessError") {
assert.fail(`Unexpected error: ${error._tag}`);
}
assert.equal(error.operation, "wait-for-exit");
assert.equal(error.mode, "dev:server");
assert.equal(error.executable, "vp");
assert.equal(error.argumentCount, 5);
assert.equal(error.shell, false);
assert.equal(error.cause, cause);
assert.ok(!error.message.includes(cause.message));
assert.notProperty(error, "args");
assert.notInclude(error.message, "secret-token-value");
});
});
});
});
Loading
Loading