1import {
2 BearerConnectionTarget,
3 PrimaryConnectionTarget,
4 RelayConnectionTarget,
5 SshConnectionTarget,
6} from "@t3tools/client-runtime/connection";
7import { buildRemoteOpenUrl, EnvironmentId } from "@t3tools/contracts";
8import { describe, expect, it } from "vite-plus/test";
9
10import { resolveRemoteOpenState } from "./remoteOpen";
11
12const environmentId = EnvironmentId.make("environment-1");
13
14const primaryTarget = (httpBaseUrl: string) =>
15 new PrimaryConnectionTarget({
16 environmentId,
17 label: "sol",
18 httpBaseUrl,
19 wsBaseUrl: httpBaseUrl.replace("http", "ws"),
20 });
21
22const TAILSCALE_TARGETS = [
23 { kind: "tailscale", host: "sol.tail1234.ts.net" },
24 { kind: "mdns", host: "sol.local" },
25] as const;
26
27describe("resolveRemoteOpenState", () => {
28 it("keeps exec behavior for a loopback primary target", () => {
29 expect(
30 resolveRemoteOpenState({
31 target: primaryTarget("http://127.0.0.1:8000"),
32 sshAlias: null,
33 isDesktopRenderer: false,
34 remoteOpenTargets: TAILSCALE_TARGETS,
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 sshAlias: null,
44 isDesktopRenderer: false,
45 remoteOpenTargets: TAILSCALE_TARGETS,
46 }),
47 ).toEqual({
48 mode: "remote-links",
49 host: { kind: "tailscale", host: "sol.tail1234.ts.net" },
50 });
51 });
52
53 it("keeps exec behavior for the desktop app's own primary even on a NAT URL", () => {
54 // wsl-only mode binds the primary to the WSL2 NAT address; it is still
55 // this machine because the desktop app manages its own primary backend.
56 expect(
57 resolveRemoteOpenState({
58 target: primaryTarget("http://172.29.112.1:14369"),
59 sshAlias: null,
60 isDesktopRenderer: true,
61 remoteOpenTargets: TAILSCALE_TARGETS,
62 }),
63 ).toEqual({ mode: "local-exec" });
64 });
65
66 it("keeps exec behavior for desktop-local secondary backends", () => {
67 expect(
68 resolveRemoteOpenState({
69 target: new BearerConnectionTarget({
70 environmentId,
71 label: "WSL (Ubuntu)",
72 connectionId: "local:wsl-1",
73 }),
74 sshAlias: null,
75 isDesktopRenderer: false,
76 remoteOpenTargets: TAILSCALE_TARGETS,
77 }),
78 ).toEqual({ mode: "local-exec" });
79 });
80
81 it("prefers the desktop SSH alias over server-advertised hosts", () => {
82 expect(
83 resolveRemoteOpenState({
84 target: new SshConnectionTarget({
85 environmentId,
86 label: "sol",
87 connectionId: "ssh-1",
88 }),
89 sshAlias: "sol",
90 isDesktopRenderer: true,
91 remoteOpenTargets: TAILSCALE_TARGETS,
92 }),
93 ).toEqual({ mode: "remote-links", host: { kind: "ssh-alias", host: "sol" } });
94 });
95
96 it("reports unavailable when a remote environment advertises no hosts", () => {
97 for (const remoteOpenTargets of [[], undefined] as const) {
98 expect(
99 resolveRemoteOpenState({
100 target: new RelayConnectionTarget({ environmentId, label: "sol" }),
101 sshAlias: null,
102 isDesktopRenderer: false,
103 remoteOpenTargets,
104 }),
105 ).toEqual({ mode: "remote-unavailable" });
106 }
107 });
108
109 it("falls back to exec when the environment has no catalog entry", () => {
110 expect(
111 resolveRemoteOpenState({
112 target: null,
113 sshAlias: null,
114 isDesktopRenderer: false,
115 remoteOpenTargets: undefined,
116 }),
117 ).toEqual({ mode: "local-exec" });
118 });
119});
120
121describe("buildRemoteOpenUrl", () => {
122 it("builds a vscode-remote deep link", () => {
123 expect(
124 buildRemoteOpenUrl({
125 editor: "vscode",
126 host: "sol.tail1234.ts.net",
127 absolutePath: "/home/theo/code/my repo",
128 }),
129 ).toBe("vscode://vscode-remote/ssh-remote+sol.tail1234.ts.net/home/theo/code/my%20repo");
130 });
131
132 it("uses the fork's scheme", () => {
133 expect(buildRemoteOpenUrl({ editor: "cursor", host: "sol", absolutePath: "/tmp/x" })).toBe(
134 "cursor://vscode-remote/ssh-remote+sol/tmp/x",
135 );
136 });
137
138 it("roots Windows paths", () => {
139 expect(
140 buildRemoteOpenUrl({ editor: "vscode", host: "sol", absolutePath: "C:\\Users\\theo" }),
141 ).toBe("vscode://vscode-remote/ssh-remote+sol/C%3A/Users/theo");
142 });
143
144 it("builds Zed's ssh deep link", () => {
145 expect(
146 buildRemoteOpenUrl({
147 editor: "zed",
148 host: "sol.tail1234.ts.net",
149 absolutePath: "/home/theo/code/my repo",
150 }),
151 ).toBe("zed://ssh/sol.tail1234.ts.net/home/theo/code/my%20repo");
152 });
153
154 it("drops the Windows drive letter for Zed", () => {
155 expect(
156 buildRemoteOpenUrl({ editor: "zed", host: "sol", absolutePath: "C:\\Users\\theo" }),
157 ).toBe("zed://ssh/sol/Users/theo");
158 expect(buildRemoteOpenUrl({ editor: "zed", host: "sol", absolutePath: "/C:/project" })).toBe(
159 "zed://ssh/sol/C%3A/project",
160 );
161 });
162
163 it("builds a Toolbox ssh deep link for JetBrains IDEs", () => {
164 expect(
165 buildRemoteOpenUrl({
166 editor: "pycharm",
167 host: "sol.tail1234.ts.net",
168 absolutePath: "/home/theo/code/my repo",
169 }),
170 ).toBe(
171 "jetbrains://gateway/ssh/environment?h=sol.tail1234.ts.net&launchIde=true&ideHint=PY&projectHint=%2Fhome%2Ftheo%2Fcode%2Fmy+repo",
172 );
173 expect(
174 buildRemoteOpenUrl({ editor: "idea", host: "sol", absolutePath: "C:\\Users\\theo" }),
175 ).toBe(
176 "jetbrains://gateway/ssh/environment?h=sol&launchIde=true&ideHint=IU&projectHint=C%3A%2FUsers%2Ftheo",
177 );
178 });
179
180 it("returns undefined for editors without remote support", () => {
181 expect(buildRemoteOpenUrl({ editor: "kiro", host: "sol", absolutePath: "/tmp/x" })).toBe(
182 undefined,
183 );
184 });
185});