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, createDevRunnerEnv, findFirstAvailableOffset, 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", () => Effect.sync(() => { assert.deepStrictEqual(getDevRunnerModeArgs("dev:desktop"), [ "run", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "dev", ]); }), ); it.effect("places Vite+ run flags before the task name", () => Effect.sync(() => { assert.deepStrictEqual(getDevRunnerModeArgs("dev"), [ "run", "--filter=@t3tools/contracts", "--filter=@t3tools/web", "--filter=t3", "--parallel", "dev", ]); }), ); }); describe("resolveOffset", () => { it.effect("uses explicit T3CODE_PORT_OFFSET when provided", () => Effect.gen(function* () { const result = yield* resolveOffset({ portOffset: 12, devInstance: undefined }); assert.deepStrictEqual(result, { offset: 12, source: "T3CODE_PORT_OFFSET=12", }); }), ); it.effect("hashes non-numeric instance values", () => Effect.gen(function* () { const result = yield* resolveOffset({ portOffset: undefined, devInstance: "feature-branch", }); assert.ok(result.offset >= 1); assert.ok(result.offset <= 3000); }), ); it.effect("returns structured context for a negative port offset", () => Effect.gen(function* () { const error = yield* resolveOffset({ portOffset: -1, devInstance: undefined }).pipe( Effect.flip, ); 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)); }), ); }); describe("createDevRunnerEnv", () => { it.effect("defaults T3CODE_HOME to ~/.t3 when not provided", () => Effect.gen(function* () { const path = yield* Path.Path; const env = yield* createDevRunnerEnv({ mode: "dev", baseEnv: {}, serverOffset: 0, webOffset: 0, t3Home: undefined, noBrowser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, port: undefined, devUrl: undefined, }); assert.equal(env.T3CODE_HOME, path.resolve(NodeOS.homedir(), ".t3")); }), ); it.effect("supports explicit typed overrides", () => Effect.gen(function* () { const path = yield* Path.Path; const env = yield* createDevRunnerEnv({ mode: "dev:server", baseEnv: {}, serverOffset: 0, webOffset: 0, t3Home: "/tmp/custom-t3", noBrowser: true, autoBootstrapProjectFromCwd: false, logWebSocketEvents: true, host: "0.0.0.0", port: 4222, devUrl: new URL("http://localhost:7331"), }); assert.equal(env.T3CODE_HOME, path.resolve("/tmp/custom-t3")); assert.equal(env.T3CODE_PORT, "4222"); assert.equal(env.VITE_HTTP_URL, "http://localhost:4222"); assert.equal(env.VITE_WS_URL, "ws://localhost:4222"); assert.equal(env.T3CODE_NO_BROWSER, "1"); assert.equal(env.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD, "0"); assert.equal(env.T3CODE_LOG_WS_EVENTS, "1"); assert.equal(env.T3CODE_HOST, "0.0.0.0"); assert.equal(env.VITE_DEV_SERVER_URL, "http://localhost:7331/"); }), ); it.effect("does not force websocket logging on in dev mode when unset", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ mode: "dev", baseEnv: { T3CODE_LOG_WS_EVENTS: "keep-me-out", }, serverOffset: 0, webOffset: 0, t3Home: undefined, noBrowser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, port: undefined, devUrl: undefined, }); assert.equal(env.T3CODE_MODE, "web"); assert.equal(env.T3CODE_LOG_WS_EVENTS, undefined); }), ); it.effect("forwards explicit websocket logging false without coercing it away", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ mode: "dev", baseEnv: { T3CODE_LOG_WS_EVENTS: "1", }, serverOffset: 0, webOffset: 0, t3Home: undefined, noBrowser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: false, host: undefined, port: undefined, devUrl: undefined, }); assert.equal(env.T3CODE_LOG_WS_EVENTS, "0"); }), ); it.effect("uses custom t3Home when provided", () => Effect.gen(function* () { const path = yield* Path.Path; const env = yield* createDevRunnerEnv({ mode: "dev", baseEnv: {}, serverOffset: 0, webOffset: 0, t3Home: "/tmp/my-t3", noBrowser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, port: undefined, devUrl: undefined, }); assert.equal(env.T3CODE_HOME, path.resolve("/tmp/my-t3")); }), ); it.effect("pins desktop dev to a stable backend port and websocket url", () => Effect.gen(function* () { const path = yield* Path.Path; const env = yield* createDevRunnerEnv({ mode: "dev:desktop", baseEnv: { T3CODE_PORT: "13773", T3CODE_MODE: "web", T3CODE_NO_BROWSER: "0", T3CODE_HOST: "0.0.0.0", VITE_DEV_SERVER_URL: "http://127.0.0.1:8526", VITE_WS_URL: "ws://localhost:13773", }, serverOffset: 0, webOffset: 0, t3Home: "/tmp/my-t3", noBrowser: true, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: "127.0.0.1", port: 4222, devUrl: undefined, }); assert.equal(env.T3CODE_HOME, path.resolve("/tmp/my-t3")); assert.equal(env.PORT, "5733"); assert.equal(env.VITE_DEV_SERVER_URL, "http://127.0.0.1:5733"); assert.equal(env.HOST, "127.0.0.1"); assert.equal(env.T3CODE_PORT, "4222"); assert.equal(env.VITE_HTTP_URL, "http://127.0.0.1:4222"); assert.equal(env.T3CODE_MODE, undefined); assert.equal(env.T3CODE_NO_BROWSER, undefined); assert.equal(env.T3CODE_HOST, undefined); assert.equal(env.VITE_WS_URL, "ws://127.0.0.1:4222"); }), ); it.effect("defaults dev server mode to the higher backend port range", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ mode: "dev", baseEnv: {}, serverOffset: 0, webOffset: 0, t3Home: undefined, noBrowser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, port: undefined, devUrl: undefined, }); assert.equal(env.T3CODE_PORT, "13773"); assert.equal(env.VITE_HTTP_URL, "http://localhost:13773"); assert.equal(env.VITE_WS_URL, "ws://localhost:13773"); }), ); }); describe("findFirstAvailableOffset", () => { it.effect("returns the starting offset when required ports are available", () => Effect.gen(function* () { const offset = yield* findFirstAvailableOffset({ startOffset: 0, requireServerPort: true, requireWebPort: true, checkPortAvailability: () => Effect.succeed(true), }); assert.equal(offset, 0); }), ); it.effect("advances until all required ports are available", () => Effect.gen(function* () { const taken = new Set([13773, 5733, 13774, 5734]); const offset = yield* findFirstAvailableOffset({ startOffset: 0, requireServerPort: true, requireWebPort: true, checkPortAvailability: (port) => Effect.succeed(!taken.has(port)), }); assert.equal(offset, 2); }), ); it.effect("allows offsets where the non-required server port exceeds max", () => Effect.gen(function* () { const offset = yield* findFirstAvailableOffset({ startOffset: 59_802, requireServerPort: false, requireWebPort: true, checkPortAvailability: () => Effect.succeed(true), }); 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", () => { it.effect("checks overlapping hosts sequentially to avoid self-interference", () => Effect.gen(function* () { let inFlightCount = 0; const calls: Array<[number, string]> = []; const available = yield* checkPortAvailabilityOnHosts( 13_773, ["127.0.0.1", "0.0.0.0", "::"], (port, host) => Effect.promise(async () => { calls.push([port, host]); inFlightCount += 1; const overlapped = inFlightCount > 1; await Promise.resolve(); inFlightCount -= 1; return !overlapped; }), ); assert.equal(available, true); assert.deepStrictEqual(calls, [ [13_773, "127.0.0.1"], [13_773, "0.0.0.0"], [13_773, "::"], ]); }), ); }); describe("resolveModePortOffsets", () => { it.effect("uses a shared fallback offset for dev mode", () => Effect.gen(function* () { const taken = new Set([13773, 5733]); const offsets = yield* resolveModePortOffsets({ mode: "dev", startOffset: 0, hasExplicitServerPort: false, hasExplicitDevUrl: false, checkPortAvailability: (port) => Effect.succeed(!taken.has(port)), }); assert.deepStrictEqual(offsets, { serverOffset: 1, webOffset: 1 }); }), ); it.effect("keeps server offset stable for dev:web and only shifts web offset", () => Effect.gen(function* () { const taken = new Set([5733]); const offsets = yield* resolveModePortOffsets({ mode: "dev:web", startOffset: 0, hasExplicitServerPort: false, hasExplicitDevUrl: false, checkPortAvailability: (port) => Effect.succeed(!taken.has(port)), }); assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 1 }); }), ); it.effect("shifts only server offset for dev:server", () => Effect.gen(function* () { const taken = new Set([13773]); const offsets = yield* resolveModePortOffsets({ mode: "dev:server", startOffset: 0, hasExplicitServerPort: false, hasExplicitDevUrl: false, checkPortAvailability: (port) => Effect.succeed(!taken.has(port)), }); assert.deepStrictEqual(offsets, { serverOffset: 1, webOffset: 1 }); }), ); it.effect("respects explicit dev-url override for dev:web", () => Effect.gen(function* () { const offsets = yield* resolveModePortOffsets({ mode: "dev:web", startOffset: 0, hasExplicitServerPort: false, hasExplicitDevUrl: true, checkPortAvailability: () => Effect.succeed(false), }); assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 0 }); }), ); it.effect("respects explicit server port override for dev:server", () => Effect.gen(function* () { const offsets = yield* resolveModePortOffsets({ mode: "dev:server", startOffset: 0, hasExplicitServerPort: true, hasExplicitDevUrl: false, checkPortAvailability: () => Effect.succeed(false), }); assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 0 }); }), ); }); 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"); }); }); }); });