Skip to content
Source
Sign inSign up

remoteOpen.test.ts

288 lines10.8 KB

1import {
2 PrimaryConnectionTarget,
3 RelayConnectionTarget,
4 SshConnectionTarget,
5} from "@t3tools/client-runtime/connection";
6import { buildRemoteOpenUrl, EnvironmentId } from "@t3tools/contracts";
7import { describe, expect, it } from "vite-plus/test";
8​
9import { remoteFallbackEditors, resolveRemoteOpenState } from "./remoteOpen";
10​
11const environmentId = EnvironmentId.make("environment-1");
12​
13const primaryTarget = (httpBaseUrl: string) =>
14 new PrimaryConnectionTarget({
15 environmentId,
16 label: "sol",
17 httpBaseUrl,
18 wsBaseUrl: httpBaseUrl.replace("http", "ws"),
19 });
20​
21const TAILSCALE_TARGETS = [
22 { kind: "tailscale", host: "sol.tail1234.ts.net" },
23 { kind: "mdns", host: "sol.local" },
24] as const;
25​
26describe("resolveRemoteOpenState", () => {
27 it("keeps exec behavior for a loopback primary target", () => {
28 expect(
29 resolveRemoteOpenState({
30 target: primaryTarget("http://127.0.0.1:8000"),
31 sshTarget: null,
32 isDesktopRenderer: false,
33 remoteOpenTargets: TAILSCALE_TARGETS,
34 hostEnvironmentId: null,
35 }),
36 ).toEqual({ mode: "local-exec" });
37 });
38​
39 it("uses deep links for a primary target reached over the network", () => {
40 expect(
41 resolveRemoteOpenState({
42 target: primaryTarget("https://sol.tail1234.ts.net"),
43 sshTarget: null,
44 isDesktopRenderer: false,
45 remoteOpenTargets: TAILSCALE_TARGETS,
46 hostEnvironmentId: null,
47 }),
48 ).toEqual({
49 mode: "remote-links",
50 host: { kind: "tailscale", host: "sol.tail1234.ts.net" },
51 });
52 });
53​
54 it("keeps exec behavior for the desktop app's own primary even on a NAT URL", () => {
55 // wsl-only binds the desktop-owned primary to its WSL2 NAT address.
56 expect(
57 resolveRemoteOpenState({
58 target: primaryTarget("http://172.29.112.1:14369"),
59 sshTarget: null,
60 isDesktopRenderer: true,
61 remoteOpenTargets: TAILSCALE_TARGETS,
62 hostEnvironmentId: null,
63 }),
64 ).toEqual({ mode: "local-exec" });
65 });
66​
67 it("keeps exec behavior for the environment that owns the desktop's T3 home", () => {
68 // With Local environment off, a background service on this machine can own the
69 // desktop's home and still be reached through T3 Connect.
70 expect(
71 resolveRemoteOpenState({
72 target: new RelayConnectionTarget({ environmentId, label: "sol" }),
73 sshTarget: null,
74 isDesktopRenderer: true,
75 remoteOpenTargets: TAILSCALE_TARGETS,
76 hostEnvironmentId: environmentId,
77 }),
78 ).toEqual({ mode: "local-exec" });
79 });
80​
81 it("uses deep links for other environments when the desktop home has its own", () => {
82 expect(
83 resolveRemoteOpenState({
84 target: new RelayConnectionTarget({ environmentId, label: "sol" }),
85 sshTarget: null,
86 isDesktopRenderer: true,
87 remoteOpenTargets: TAILSCALE_TARGETS,
88 hostEnvironmentId: EnvironmentId.make("environment-host"),
89 }),
90 ).toEqual({
91 mode: "remote-links",
92 host: { kind: "tailscale", host: "sol.tail1234.ts.net" },
93 });
94 });
95​
96 it("leaves a bare desktop SSH alias for the editor to resolve through SSH config", () => {
97 const state = resolveRemoteOpenState({
98 target: new SshConnectionTarget({ environmentId, label: "sol", connectionId: "ssh-1" }),
99 sshTarget: { alias: "sol", username: null },
100 isDesktopRenderer: true,
101 remoteOpenTargets: [
102 { kind: "tailscale", host: "sol.tail1234.ts.net", username: "server-user" },
103 ],
104 hostEnvironmentId: null,
105 });
106 expect(state).toEqual({ mode: "remote-links", host: { kind: "ssh-alias", host: "sol" } });
107 if (state.mode !== "remote-links") throw new Error("Expected a remote link");
108 expect(
109 buildRemoteOpenUrl({ editor: "vscode", ...state.host, absolutePath: "/work/project" }),
110 ).toBe("vscode://vscode-remote/ssh-remote+sol/work/project");
111 expect(
112 buildRemoteOpenUrl({ editor: "zed", ...state.host, absolutePath: "/work/project" }),
113 ).toBe("zed://ssh/sol/work/project");
114 expect(
115 buildRemoteOpenUrl({ editor: "idea", ...state.host, absolutePath: "/work/project" }),
116 ).toBe(
117 "jetbrains://gateway/ssh/environment?h=sol&launchIde=true&ideHint=IU&projectHint=%2Fwork%2Fproject",
118 );
119 });
120​
121 it.each([
122 { alias: "server", username: "admin-user" },
123 { alias: "admin-user@server", username: null },
124 { alias: "old-user@server", username: "admin-user" },
125 { alias: "admin-user@server", username: "" },
126 { alias: "admin-user@server", username: " " },
127 ])("keeps one login username in editor links for $alias (username: '$username')", (sshTarget) => {
128 const state = resolveRemoteOpenState({
129 target: new SshConnectionTarget({ environmentId, label: "server", connectionId: "ssh-1" }),
130 sshTarget,
131 isDesktopRenderer: true,
132 remoteOpenTargets: TAILSCALE_TARGETS,
133 hostEnvironmentId: null,
134 });
135 expect(state.mode).toBe("remote-links");
136 if (state.mode !== "remote-links") throw new Error("Expected a remote link");
137 expect(
138 buildRemoteOpenUrl({ editor: "vscode", ...state.host, absolutePath: "/work/project" }),
139 ).toBe("vscode://vscode-remote/ssh-remote+admin-user%40server/work/project");
140 expect(
141 buildRemoteOpenUrl({ editor: "zed", ...state.host, absolutePath: "/work/project" }),
142 ).toBe("zed://ssh/admin-user@server/work/project");
143 expect(
144 buildRemoteOpenUrl({ editor: "idea", ...state.host, absolutePath: "/work/project" }),
145 ).toBe(
146 "jetbrains://gateway/ssh/environment?h=server&launchIde=true&ideHint=IU&projectHint=%2Fwork%2Fproject&u=admin-user",
147 );
148 });
149​
150 it("uses the advertised login username in editor links over a relay", () => {
151 const state = resolveRemoteOpenState({
152 target: new RelayConnectionTarget({ environmentId, label: "server" }),
153 sshTarget: null,
154 isDesktopRenderer: false,
155 remoteOpenTargets: [
156 { kind: "tailscale", host: "server.tail1234.ts.net", username: "admin-user" },
157 ],
158 hostEnvironmentId: null,
159 });
160 expect(state.mode).toBe("remote-links");
161 if (state.mode !== "remote-links") throw new Error("Expected a remote link");
162 expect(
163 buildRemoteOpenUrl({ editor: "cursor", ...state.host, absolutePath: "/work/project" }),
164 ).toBe("cursor://vscode-remote/ssh-remote+admin-user%40server.tail1234.ts.net/work/project");
165 });
166​
167 it("reports unavailable when a remote environment advertises no hosts", () => {
168 for (const remoteOpenTargets of [[], undefined] as const) {
169 expect(
170 resolveRemoteOpenState({
171 target: new RelayConnectionTarget({ environmentId, label: "sol" }),
172 sshTarget: null,
173 isDesktopRenderer: false,
174 remoteOpenTargets,
175 hostEnvironmentId: null,
176 }),
177 ).toEqual({ mode: "remote-unavailable" });
178 }
179 });
180});
181​
182describe("remoteFallbackEditors", () => {
183 it.each([
184 {
185 // iPadOS Safari requests the desktop site with a Macintosh user agent by default.
186 device: "iPad with a desktop user agent",
187 userAgent:
188 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
189 platform: "MacIntel",
190 maxTouchPoints: 5,
191 },
192 {
193 device: "iPhone",
194 userAgent:
195 "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
196 platform: "iPhone",
197 maxTouchPoints: 5,
198 },
199 {
200 device: "Android phone",
201 userAgent:
202 "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36",
203 platform: "Linux armv81",
204 maxTouchPoints: 5,
205 },
206 ])("offers no desktop editor for remote links on an $device", ({ device: _, ...identity }) => {
207 expect(remoteFallbackEditors(identity)).toEqual([]);
208 });
209​
210 it("keeps the VS Code fallback for remote links on a Mac", () => {
211 expect(
212 remoteFallbackEditors({
213 userAgent:
214 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
215 platform: "MacIntel",
216 maxTouchPoints: 0,
217 }),
218 ).toEqual(["vscode"]);
219 });
220});
221​
222describe("buildRemoteOpenUrl", () => {
223 it("roots Windows paths", () => {
224 expect(
225 buildRemoteOpenUrl({ editor: "vscode", host: "sol", absolutePath: "C:\\Users\\theo" }),
226 ).toBe("vscode://vscode-remote/ssh-remote+sol/C%3A/Users/theo");
227 });
228​
229 it("keeps a plus in the SSH username through Remote-SSH's authority parsing", () => {
230 const url = buildRemoteOpenUrl({
231 editor: "vscode",
232 host: "ssh.example.com",
233 username: "vm+demo",
234 absolutePath: "/workspace/project",
235 });
236 // VS Code decodes the authority, then Remote-SSH (0.129) takes `authority.split("+")[1]` and
237 // reads it as hex-encoded JSON `{ hostName, user }` or as `user@host`.
238 const authority = decodeURIComponent(new URL(url ?? "").pathname.split("/")[1] ?? "");
239 const destination = authority.split("+")[1] ?? "";
240 const hexBytes = /^(?:[0-9a-f]{2})+$/i.test(destination)
241 ? destination.match(/../g)?.map((byte) => Number.parseInt(byte, 16))
242 : undefined;
243 const resolved =
244 hexBytes === undefined
245 ? destination.match(/^(?:(?<user>.*)@)?(?<hostName>.+?)$/)?.groups
246 : JSON.parse(new TextDecoder().decode(new Uint8Array(hexBytes)));
247 expect({ user: resolved?.user, hostName: resolved?.hostName }).toEqual({
248 user: "vm+demo",
249 hostName: "ssh.example.com",
250 });
251 });
252​
253 it("drops the Windows drive letter for Zed", () => {
254 expect(
255 buildRemoteOpenUrl({ editor: "zed", host: "sol", absolutePath: "C:\\Users\\theo" }),
256 ).toBe("zed://ssh/sol/Users/theo");
257 expect(buildRemoteOpenUrl({ editor: "zed", host: "sol", absolutePath: "/C:/project" })).toBe(
258 "zed://ssh/sol/C%3A/project",
259 );
260 });
261​
262 it("adds a file position only for confirmed VS Code-family file targets", () => {
263 const targets = [
264 ["vscode", true, "vscode://vscode-remote/ssh-remote+sol/tmp/my%20file.md:1:1"],
265 ["cursor", true, "cursor://vscode-remote/ssh-remote+sol/tmp/my%20file.md:1:1"],
266 ["vscode", false, "vscode://vscode-remote/ssh-remote+sol/tmp/my%20file.md"],
267 ["vscode", undefined, "vscode://vscode-remote/ssh-remote+sol/tmp/my%20file.md"],
268 ["zed", true, "zed://ssh/sol/tmp/my%20file.md"],
269 [
270 "pycharm",
271 true,
272 "jetbrains://gateway/ssh/environment?h=sol&launchIde=true&ideHint=PY&projectHint=%2Ftmp%2Fmy+file.md",
273 ],
274 ] as const;
275​
276 for (const [editor, isFile, expected] of targets) {
277 expect(
278 buildRemoteOpenUrl({
279 editor,
280 host: "sol",
281 absolutePath: "/tmp/my file.md",
282 ...(isFile === undefined ? {} : { isFile }),
283 }),
284 `${editor}, isFile=${isFile}`,
285 ).toBe(expected);
286 }
287 });
288});