1import { expect, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as Effect from "effect/Effect";
4import * as Sink from "effect/Sink";
5import * as Stream from "effect/Stream";
6import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
7import {
8 isLocalSshDeviceHost,
9 LocalDeviceHostAddresses,
10 remoteSshDeviceHosts,
11} from "./localSshDeviceHost.ts";
12
13const host = (target: string, port?: number) => ({
14 id: target,
15 label: target,
16 target,
17 ...(port ? { port } : {}),
18});
19const spawner = ChildProcessSpawner.make((command) =>
20 Effect.gen(function* () {
21 if (command._tag !== "StandardCommand") return yield* Effect.die("Unexpected command");
22 // Any attempt to actually connect fails this test.
23 expect(command.args).toContain("-G");
24 const target = command.args.at(-1);
25 const configs: Record<string, string> = {
26 "mac-mini": "hostname 100.65.180.100\nport 22\n",
27 remote: "hostname 192.0.2.1\nport 22\n",
28 loopback: "hostname 127.0.1.1\nport 22\n",
29 ipv6: "hostname ::1\nport 22\n",
30 forwarded: "hostname 127.0.0.1\nport 2222\n",
31 proxy: "hostname 127.0.0.1\nport 22\nproxyjump bastion\n",
32 command: "hostname 127.0.0.1\nport 22\nproxycommand nc remote 22\n",
33 unresolved: "hostname example.invalid\nport 22\n",
34 };
35 return ChildProcessSpawner.makeHandle({
36 pid: ChildProcessSpawner.ProcessId(123),
37 stdout: Stream.make(new TextEncoder().encode(configs[target ?? ""] ?? "")),
38 stderr: Stream.empty,
39 all: Stream.empty,
40 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
41 isRunning: Effect.succeed(false),
42 kill: () => Effect.void,
43 stdin: Sink.drain,
44 getInputFd: () => Sink.drain,
45 getOutputFd: () => Stream.empty,
46 unref: Effect.succeed(Effect.void),
47 });
48 }),
49);
50const provide = <A, E>(
51 effect: Effect.Effect<A, E, Effect.Services<ReturnType<typeof isLocalSshDeviceHost>>>,
52) =>
53 effect.pipe(
54 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
55 Effect.provideService(LocalDeviceHostAddresses, new Set(["100.65.180.100"])),
56 Effect.provide(NodeServices.layer),
57 );
58
59it.effect("skips SSH aliases resolving to this machine, including loopback", () =>
60 provide(
61 Effect.gen(function* () {
62 for (const target of ["mac-mini", "loopback", "ipv6"]) {
63 expect(yield* isLocalSshDeviceHost(host(target))).toBe(true);
64 }
65 }),
66 ),
67);
68
69it.effect("keeps remote, forwarded, proxied, and unresolved destinations", () =>
70 provide(
71 Effect.gen(function* () {
72 for (const target of ["remote", "forwarded", "proxy", "command", "unresolved"]) {
73 expect(yield* isLocalSshDeviceHost(host(target))).toBe(false);
74 }
75 }),
76 ),
77);
78
79it.effect("removes only self targets from a fanned-out host list", () =>
80 provide(
81 Effect.gen(function* () {
82 expect(
83 yield* remoteSshDeviceHosts([host("mac-mini"), host("remote"), host("forwarded")]),
84 ).toEqual([host("remote"), host("forwarded")]);
85 }),
86 ),
87);