1import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, describe, it } from "@effect/vitest";
4import * as NetService from "@t3tools/shared/Net";
5import { SshPasswordPromptError } from "@t3tools/ssh/errors";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as Path from "effect/Path";
10
11import * as DesktopSshEnvironment from "./DesktopSshEnvironment.ts";
12import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts";
13
14function makeTempHomeDir() {
15 return Effect.gen(function* () {
16 const fs = yield* FileSystem.FileSystem;
17 return yield* fs.makeTempDirectoryScoped({ prefix: "t3-ssh-env-test-" });
18 });
19}
20
21describe("sshEnvironment", () => {
22 it("keeps prompt presentation diagnostics distinct from the legacy wrapper message", () => {
23 const cause = new DesktopSshPasswordPrompts.DesktopSshPromptPresentationError({
24 requestId: "prompt-1",
25 destination: "devbox",
26 operation: "send-prompt-request",
27 cause: new Error("renderer send failed"),
28 });
29
30 assert.equal(cause.message, "Failed to present SSH password prompt for devbox.");
31 assert.equal(
32 DesktopSshEnvironment.toSshPasswordPromptError(cause).message,
33 "The Knife window is not available for SSH authentication.",
34 );
35 });
36
37 it("treats password prompt timeouts as cancellable authentication prompts", () => {
38 assert.equal(
39 DesktopSshEnvironment.isDesktopSshPasswordPromptCancellation(
40 new SshPasswordPromptError({
41 message: "SSH authentication timed out for devbox.",
42 cause: new DesktopSshPasswordPrompts.DesktopSshPromptTimedOutError({
43 requestId: "prompt-1",
44 destination: "devbox",
45 }),
46 }),
47 ),
48 true,
49 );
50 });
51
52 it.effect("wires desktop host discovery through the ssh package runtime", () =>
53 Effect.gen(function* () {
54 const fs = yield* FileSystem.FileSystem;
55 const path = yield* Path.Path;
56 const homeDir = yield* makeTempHomeDir();
57 const sshDir = path.join(homeDir, ".ssh");
58 yield* fs.makeDirectory(path.join(sshDir, "config.d"), { recursive: true });
59 yield* fs.writeFileString(
60 path.join(sshDir, "config"),
61 ["Host devbox", " HostName devbox.example.com", "Include config.d/*.conf", ""].join("\n"),
62 );
63 yield* fs.writeFileString(
64 path.join(sshDir, "config.d", "team.conf"),
65 [
66 "Host staging",
67 " HostName staging.example.com",
68 "Host *",
69 " ServerAliveInterval 30",
70 "",
71 ].join("\n"),
72 );
73 yield* fs.writeFileString(
74 path.join(sshDir, "known_hosts"),
75 [
76 "known.example.com ssh-ed25519 AAAA",
77 "|1|hashed|entry ssh-ed25519 AAAA",
78 "[bastion.example.com]:2222 ssh-ed25519 AAAA",
79 "",
80 ].join("\n"),
81 );
82
83 const sshEnvironment = yield* DesktopSshEnvironment.DesktopSshEnvironment;
84 const hosts = yield* sshEnvironment.discoverHosts({ homeDir });
85 assert.deepEqual(hosts, [
86 {
87 alias: "bastion.example.com",
88 hostname: "bastion.example.com",
89 username: null,
90 port: null,
91 source: "known-hosts",
92 },
93 {
94 alias: "devbox",
95 hostname: "devbox",
96 username: null,
97 port: null,
98 source: "ssh-config",
99 },
100 {
101 alias: "known.example.com",
102 hostname: "known.example.com",
103 username: null,
104 port: null,
105 source: "known-hosts",
106 },
107 {
108 alias: "staging",
109 hostname: "staging",
110 username: null,
111 port: null,
112 source: "ssh-config",
113 },
114 ]);
115 }).pipe(
116 Effect.provide(
117 DesktopSshEnvironment.layer().pipe(
118 Layer.provideMerge(
119 Layer.succeed(DesktopSshPasswordPrompts.DesktopSshPasswordPrompts, {
120 request: () => Effect.die("unexpected password prompt request"),
121 resolve: () => Effect.die("unexpected password prompt resolution"),
122 }),
123 ),
124 Layer.provideMerge(NodeServices.layer),
125 Layer.provideMerge(NodeHttpClient.layerUndici),
126 Layer.provideMerge(NetService.layer),
127 ),
128 ),
129 Effect.scoped,
130 ),
131 );
132});