import * as NodeNet from "node:net"; import { it as effectIt } from "@effect/vitest";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Net from "@t3tools/shared/Net";import * as Cause from "effect/Cause";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as PlatformError from "effect/PlatformError";import { expect } from "vite-plus/test"; import * as ProcessRunner from "../processRunner.ts";import * as PortScanner from "./PortScanner.ts";const TestProcessRunner = Layer.succeed(ProcessRunner.ProcessRunner, { run: (input) => Effect.fail( new ProcessRunner.ProcessSpawnError({ command: input.command, argumentCount: input.args.length, cwd: input.cwd, cause: PlatformError.systemError({ _tag: "NotFound", module: "ChildProcess", method: "spawn", description: "PowerShell is not installed in the test environment", }), }), ),}); const makeProbeFailureLayer = (run: ProcessRunner.ProcessRunner["Service"]["run"]) => PortScanner.layer.pipe( Layer.provide( Layer.mergeAll( Layer.succeed(ProcessRunner.ProcessRunner, { run }), Layer.succeed(Net.NetService, { canListenOnHost: () => Effect.succeed(true), isPortAvailableOnLoopback: () => Effect.succeed(true), reserveLoopbackPort: () => Effect.succeed(40_000), findAvailablePort: (preferred) => Effect.succeed(preferred), }), Layer.succeed(HostProcessPlatform, "linux"), ), ), ); const TestPortDiscoveryLive = PortScanner.layer.pipe( Layer.provide( Layer.mergeAll(TestProcessRunner, Net.layer, Layer.succeed(HostProcessPlatform, "win32")), ),); const openServer = (port: number): Effect.Effect<NodeNet.Server | null> => Effect.callback((resume) => { const server = NodeNet.createServer(); server.once("error", () => { resume(Effect.succeed(null)); }); server.listen(port, "127.0.0.1", () => { resume(Effect.succeed(server)); }); return Effect.sync(() => { server.close(); }); }); const closeServer = (server: NodeNet.Server): Effect.Effect<void> => Effect.callback((resume) => { server.close(() => resume(Effect.void)); }); const openCommonDevServer = Effect.fn("PortScannerTest.openCommonDevServer")(function* ( ports: ReadonlyArray<number>,) { for (const port of ports) { const server = yield* openServer(port); if (server !== null) return { port, server }; } return yield* Effect.die( new Error("No common development port was available for the preview scanner test"), );}); const commonDevServer = Effect.acquireRelease( openCommonDevServer(PortScanner.COMMON_DEV_PORTS), ({ server }) => closeServer(server),); /** * Integration tests against a real TCP listener. We provide the Windows host * platform so the tests exercise the TCP-probe fallback without depending on * `lsof` being installed. */effectIt.layer(TestPortDiscoveryLive)("PortDiscovery integration (TCP probe fallback)", (it) => { it.effect( "scan() returns a server we just opened on a curated dev port", Effect.fn("PortScannerTest.scanFindsCommonDevServer")(function* () { const { port } = yield* commonDevServer; const scanner = yield* PortScanner.PortDiscovery; const result = yield* scanner.scan(); const found = result.find((server) => server.port === port); expect(found).toBeDefined(); expect(found?.host).toBe("localhost"); }), ); it.effect( "retain drives an immediate broadcast to subscribers", Effect.fn("PortScannerTest.retainBroadcastsImmediately")(function* () { const { port } = yield* commonDevServer; const received: number[] = []; const scanner = yield* PortScanner.PortDiscovery; yield* scanner.subscribe((servers) => Effect.sync(() => { for (const server of servers) received.push(server.port); }), ); yield* scanner.retain; expect(received).toContain(port); }), );}); effectIt("does not swallow process probe defects", () => Effect.gen(function* () { const defect = new Error("unexpected process probe defect"); const layer = makeProbeFailureLayer(() => Effect.die(defect)); const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe( Effect.provide(layer), Effect.exit, ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { expect(Cause.hasDies(exit.cause)).toBe(true); expect(Cause.squash(exit.cause)).toBe(defect); } }),); effectIt("does not swallow process probe interruption", () => Effect.gen(function* () { const layer = makeProbeFailureLayer(() => Effect.interrupt); const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe( Effect.provide(layer), Effect.exit, ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true); } }),);
import * as NodeNet from "node:net";
import { it as effectIt } from "@effect/vitest";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Net from "@t3tools/shared/Net";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as PlatformError from "effect/PlatformError";
import { expect } from "vite-plus/test";
import * as ProcessRunner from "../processRunner.ts";
import * as PortScanner from "./PortScanner.ts";
const TestProcessRunner = Layer.succeed(ProcessRunner.ProcessRunner, {
run: (input) =>
Effect.fail(
new ProcessRunner.ProcessSpawnError({
command: input.command,
argumentCount: input.args.length,
cwd: input.cwd,
cause: PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
description: "PowerShell is not installed in the test environment",
}),
}),
),
});
const makeProbeFailureLayer = (run: ProcessRunner.ProcessRunner["Service"]["run"]) =>
PortScanner.layer.pipe(
Layer.provide(
Layer.mergeAll(
Layer.succeed(ProcessRunner.ProcessRunner, { run }),
Layer.succeed(Net.NetService, {
canListenOnHost: () => Effect.succeed(true),
isPortAvailableOnLoopback: () => Effect.succeed(true),
reserveLoopbackPort: () => Effect.succeed(40_000),
findAvailablePort: (preferred) => Effect.succeed(preferred),
}),
Layer.succeed(HostProcessPlatform, "linux"),
),
),
);
const TestPortDiscoveryLive = PortScanner.layer.pipe(
Layer.provide(
Layer.mergeAll(TestProcessRunner, Net.layer, Layer.succeed(HostProcessPlatform, "win32")),
),
);
const openServer = (port: number): Effect.Effect<NodeNet.Server | null> =>
Effect.callback((resume) => {
const server = NodeNet.createServer();
server.once("error", () => {
resume(Effect.succeed(null));
});
server.listen(port, "127.0.0.1", () => {
resume(Effect.succeed(server));
});
return Effect.sync(() => {
server.close();
});
});
const closeServer = (server: NodeNet.Server): Effect.Effect<void> =>
Effect.callback((resume) => {
server.close(() => resume(Effect.void));
});
const openCommonDevServer = Effect.fn("PortScannerTest.openCommonDevServer")(function* (
ports: ReadonlyArray<number>,
) {
for (const port of ports) {
const server = yield* openServer(port);
if (server !== null) return { port, server };
}
return yield* Effect.die(
new Error("No common development port was available for the preview scanner test"),
);
});
const commonDevServer = Effect.acquireRelease(
openCommonDevServer(PortScanner.COMMON_DEV_PORTS),
({ server }) => closeServer(server),
);
/**
* Integration tests against a real TCP listener. We provide the Windows host
* platform so the tests exercise the TCP-probe fallback without depending on
* `lsof` being installed.
*/
effectIt.layer(TestPortDiscoveryLive)("PortDiscovery integration (TCP probe fallback)", (it) => {
it.effect(
"scan() returns a server we just opened on a curated dev port",
Effect.fn("PortScannerTest.scanFindsCommonDevServer")(function* () {
const { port } = yield* commonDevServer;
const scanner = yield* PortScanner.PortDiscovery;
const result = yield* scanner.scan();
const found = result.find((server) => server.port === port);
expect(found).toBeDefined();
expect(found?.host).toBe("localhost");
}),
);
it.effect(
"retain drives an immediate broadcast to subscribers",
Effect.fn("PortScannerTest.retainBroadcastsImmediately")(function* () {
const { port } = yield* commonDevServer;
const received: number[] = [];
const scanner = yield* PortScanner.PortDiscovery;
yield* scanner.subscribe((servers) =>
Effect.sync(() => {
for (const server of servers) received.push(server.port);
}),
);
yield* scanner.retain;
expect(received).toContain(port);
}),
);
});
effectIt("does not swallow process probe defects", () =>
Effect.gen(function* () {
const defect = new Error("unexpected process probe defect");
const layer = makeProbeFailureLayer(() => Effect.die(defect));
const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe(
Effect.provide(layer),
Effect.exit,
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(Cause.hasDies(exit.cause)).toBe(true);
expect(Cause.squash(exit.cause)).toBe(defect);
}
}),
);
effectIt("does not swallow process probe interruption", () =>
Effect.gen(function* () {
const layer = makeProbeFailureLayer(() => Effect.interrupt);
const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe(
Effect.provide(layer),
Effect.exit,
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true);
}
}),
);