Skip to content
26 changes: 26 additions & 0 deletions apps/desktop/src/electron/ElectronShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,32 @@ describe("ElectronShell", () => {
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("opens VS Code protocol URLs", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);

const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal(
"vscode://vscode-remote/tunnel+devbox/workspaces/demo",
);

assert.equal(result, true);
assert.deepEqual(openExternalMock.mock.calls, [
["vscode://vscode-remote/tunnel+devbox/workspaces/demo"],
]);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("does not open non-tunnel VS Code URLs", () =>
Effect.gen(function* () {
const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal("vscode://file/etc/passwd");

assert.equal(result, false);
assert.equal(openExternalMock.mock.calls.length, 0);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("returns false when Electron rejects openExternal", () =>
Effect.gen(function* () {
openExternalMock.mockRejectedValue(new Error("open failed"));
Expand Down
12 changes: 11 additions & 1 deletion apps/desktop/src/electron/ElectronShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ import * as Electron from "electron";

const SAFE_EXTERNAL_PROTOCOLS = new Set(["http:", "https:"]);

function isSafeVSCodeTunnelUrl(url: URL): boolean {
return (
url.protocol === "vscode:" &&
url.hostname === "vscode-remote" &&
url.pathname.startsWith("/tunnel+")
);
}

export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {
if (typeof rawUrl !== "string") {
return Option.none();
}

try {
const url = new URL(rawUrl);
return SAFE_EXTERNAL_PROTOCOLS.has(url.protocol) ? Option.some(url.href) : Option.none();
return SAFE_EXTERNAL_PROTOCOLS.has(url.protocol) || isSafeVSCodeTunnelUrl(url)
? Option.some(url.href)
: Option.none();
} catch {
return Option.none();
}
Expand Down
16 changes: 14 additions & 2 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4246,7 +4246,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
const wsUrl = yield* getWsServerUrl("/ws");
const events = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) =>
client[WS_METHODS.subscribeServerConfig]({}).pipe(Stream.take(2), Stream.runCollect),
client[WS_METHODS.subscribeServerConfig]({}).pipe(
Stream.filter(
(event) => event.type === "snapshot" || event.type === "keybindingsUpdated",
),
Stream.take(2),
Stream.runCollect,
),
),
);

Expand Down Expand Up @@ -4310,7 +4316,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
const wsUrl = yield* getWsServerUrl("/ws");
const events = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) =>
client[WS_METHODS.subscribeServerConfig]({}).pipe(Stream.take(2), Stream.runCollect),
client[WS_METHODS.subscribeServerConfig]({}).pipe(
Stream.filter(
(event) => event.type === "snapshot" || event.type === "providerStatuses",
),
Stream.take(2),
Stream.runCollect,
),
),
);

Expand Down
200 changes: 200 additions & 0 deletions apps/server/src/vscodeTunnel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";

import * as ProcessRunner from "./processRunner.ts";
import * as VSCodeTunnel from "./vscodeTunnel.ts";

describe("vscode tunnel status resolution", () => {
it.effect("parses connected status when JSON is preceded by non-JSON output", () =>
Effect.gen(function* () {
const resolved = yield* VSCodeTunnel.resolveVSCodeTunnel({
enabled: true,
});

expect(resolved.status.checked).toBe(true);
expect(resolved.status.connected).toBe(true);
expect(resolved.status.machineName).toBe("devbox");
expect(resolved.status.serviceInstalled).toBe(true);
expect(resolved.tunnel).toEqual({ machineName: "devbox" });
}).pipe(
Effect.provide(
Layer.succeed(ProcessRunner.ProcessRunner, {
run: () =>
Effect.succeed({
stdout: [
"Some informational output...",
'{"tunnel":{"name":"devbox","tunnel":"connected"},"service_installed":true}',
].join("\n"),
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
}),
}),
),
),
);

it.effect("parses connected status when JSON is pretty-printed", () =>
Effect.gen(function* () {
const resolved = yield* VSCodeTunnel.resolveVSCodeTunnel({
enabled: true,
});

expect(resolved.status.checked).toBe(true);
expect(resolved.status.connected).toBe(true);
expect(resolved.status.machineName).toBe("devbox");
expect(resolved.status.serviceInstalled).toBe(true);
expect(resolved.tunnel).toEqual({ machineName: "devbox" });
}).pipe(
Effect.provide(
Layer.succeed(ProcessRunner.ProcessRunner, {
run: () =>
Effect.succeed({
stdout: [
"Some informational output...",
"{",
' "tunnel": {',
' "name": "devbox",',
' "tunnel": "connected"',
" },",
' "service_installed": true',
"}",
"Trailing output line",
].join("\n"),
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
}),
}),
),
),
);

it.effect("accepts tunnel:null and preserves service status", () =>
Effect.gen(function* () {
const resolved = yield* VSCodeTunnel.resolveVSCodeTunnel({
enabled: true,
});

expect(resolved.tunnel).toBeNull();
expect(resolved.status).toEqual({
checked: true,
connected: false,
machineName: null,
serviceInstalled: true,
});
}).pipe(
Effect.provide(
Layer.succeed(ProcessRunner.ProcessRunner, {
run: () =>
Effect.succeed({
stdout: '{"tunnel":null,"service_installed":true}',
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
}),
}),
),
),
);

it.effect("skips unrelated JSON and parses the status payload", () =>
Effect.gen(function* () {
const resolved = yield* VSCodeTunnel.resolveVSCodeTunnel({
enabled: true,
});

expect(resolved.status.checked).toBe(true);
expect(resolved.status.connected).toBe(true);
expect(resolved.status.machineName).toBe("devbox");
expect(resolved.status.serviceInstalled).toBe(true);
expect(resolved.tunnel).toEqual({ machineName: "devbox" });
}).pipe(
Effect.provide(
Layer.succeed(ProcessRunner.ProcessRunner, {
run: () =>
Effect.succeed({
stdout: [
'{"log":"starting"}',
'{"tunnel":{"name":"devbox","tunnel":"connected"},"service_installed":true}',
].join("\n"),
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
}),
}),
),
),
);

it.effect("parses status JSON even when stray closing braces precede the payload", () =>
Effect.gen(function* () {
const resolved = yield* VSCodeTunnel.resolveVSCodeTunnel({
enabled: true,
});

expect(resolved.status.checked).toBe(true);
expect(resolved.status.connected).toBe(true);
expect(resolved.status.machineName).toBe("devbox");
expect(resolved.status.serviceInstalled).toBe(true);
expect(resolved.tunnel).toEqual({ machineName: "devbox" });
}).pipe(
Effect.provide(
Layer.succeed(ProcessRunner.ProcessRunner, {
run: () =>
Effect.succeed({
stdout: [
"noise } }",
'{"tunnel":{"name":"devbox","tunnel":"connected"},"service_installed":true}',
].join("\n"),
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
}),
}),
),
),
);

it.effect("returns unavailable status when no JSON object is present", () =>
Effect.gen(function* () {
const resolved = yield* VSCodeTunnel.resolveVSCodeTunnel({
enabled: true,
});

expect(resolved.tunnel).toBeNull();
expect(resolved.status).toEqual({
checked: true,
connected: false,
machineName: null,
serviceInstalled: null,
});
}).pipe(
Effect.provide(
Layer.succeed(ProcessRunner.ProcessRunner, {
run: () =>
Effect.succeed({
stdout: "status unavailable",
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
}),
}),
),
),
);
});
Loading
Loading