1import { describe, expect, it } from "@effect/vitest";
2import { vi } from "vite-plus/test";
3import { DESKTOP_TERMINAL_CHANNEL } from "../ipc/channels.ts";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
9import * as Stream from "effect/Stream";
10import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
11import * as DesktopConfig from "../app/DesktopConfig.ts";
12import * as ElectronWindow from "../electron/ElectronWindow.ts";
13import * as DesktopTerminalHost from "./DesktopTerminalHost.ts";
14
15const electron = await vi.hoisted(async () => {
16 const { EventEmitter } = await import("node:events");
17 const sender = Object.assign(new EventEmitter(), { mainFrame: { url: "t3code-dev://app/" } });
18 class Port extends EventEmitter {
19 messages: unknown[] = [];
20 closed = false;
21 postMessage(data: unknown) {
22 this.messages.push(data);
23 }
24 start() {}
25 close() {
26 if (!this.closed) {
27 this.closed = true;
28 this.emit("close");
29 }
30 }
31 }
32 return {
33 ipcMain: new EventEmitter(),
34 sender,
35 Port,
36 window: { webContents: sender, isDestroyed: () => false },
37 };
38});
39vi.mock("electron", () => ({
40 ipcMain: electron.ipcMain,
41 BrowserWindow: { getAllWindows: () => [electron.window] },
42}));
43
44const layerEnvironment = DesktopEnvironment.layer({
45 dirname: "/repo/apps/desktop/dist-electron",
46 homeDirectory: "/tmp/t3-terminal-test",
47 platform: "darwin",
48 processArch: "arm64",
49 appVersion: "0.0.45",
50 appPath: "/tmp/T3 Code.app",
51 isPackaged: false,
52 resourcesPath: "/tmp/resources",
53 runningUnderArm64Translation: false,
54}).pipe(
55 Layer.provide(
56 Layer.mergeAll(
57 NodeServices.layer,
58 DesktopConfig.layerTest({ VITE_DEV_SERVER_URL: "http://localhost:5733" }),
59 ),
60 ),
61);
62const layer = DesktopTerminalHost.layer.pipe(
63 Layer.provide(ElectronWindow.layer),
64 Layer.provide(layerEnvironment),
65);
66
67const event = (port: InstanceType<typeof electron.Port>, frame = electron.sender.mainFrame) => {
68 const replies: boolean[] = [];
69 let firstReply = false;
70 return {
71 sender: electron.sender,
72 senderFrame: frame,
73 ports: [port],
74 replies,
75 get returnValue() {
76 return firstReply;
77 },
78 set returnValue(value: boolean) {
79 replies.push(value);
80 if (replies.length === 1) firstReply = value;
81 },
82 };
83};
84const input = {
85 environmentId: "local-environment",
86 url: "ws://127.0.0.1:3773/ws?wsTicket=private",
87};
88const readRequest = (host: DesktopTerminalHost.DesktopTerminalHost["Service"]) =>
89 host.requests.pipe(
90 Stream.runHead,
91 Effect.map((data) => JSON.parse(new TextDecoder().decode(Option.getOrThrow(data)))),
92 Effect.forkScoped({ startImmediately: true }),
93 );
94
95describe("DesktopTerminalHost", () => {
96 it.effect("selects the native route before ready and binds the announced identity", () =>
97 Effect.gen(function* () {
98 const host = yield* DesktopTerminalHost.DesktopTerminalHost;
99 yield* host.configure("http://127.0.0.1:3773");
100 const probe = (environmentId: string, httpBaseUrl = "http://127.0.0.1:3773") => {
101 const request = event(new electron.Port());
102 electron.ipcMain.emit(`${DESKTOP_TERMINAL_CHANNEL}:available`, request, {
103 environmentId,
104 httpBaseUrl,
105 });
106 expect(request.replies).toHaveLength(1);
107 return request.returnValue;
108 };
109 expect(probe(input.environmentId)).toBe(true);
110 expect(probe(input.environmentId, "http://127.0.0.1:3774")).toBe(false);
111 yield* host.handleLine(JSON.stringify({ type: "ready", environmentId: input.environmentId }));
112 expect(probe("other-environment")).toBe(false);
113 yield* host.reset;
114 expect(probe(input.environmentId)).toBe(false);
115 }).pipe(Effect.provide(layer), Effect.scoped),
116 );
117
118 it.effect("forwards RPC frames through the private pipe and closes on navigation", () =>
119 Effect.gen(function* () {
120 const host = yield* DesktopTerminalHost.DesktopTerminalHost;
121 yield* host.configure("http://127.0.0.1:3773");
122 const port = new electron.Port();
123 const opening = yield* readRequest(host);
124 electron.ipcMain.emit(DESKTOP_TERMINAL_CHANNEL, event(port), input);
125 const request = yield* Fiber.join(opening);
126 expect(request).toEqual({ type: "open", id: request.id, ...input });
127 const data = '{"_tag":"Request","tag":"terminal.write"}';
128 const message = yield* readRequest(host);
129 port.emit("message", { data });
130 expect(yield* Fiber.join(message)).toEqual({ type: "message", id: request.id, data });
131 yield* host.handleLine(JSON.stringify({ type: "message", id: request.id, data: "response" }));
132 expect(port.messages).toEqual([{ type: "message", id: request.id, data: "response" }]);
133 const closing = yield* readRequest(host);
134 electron.sender.emit("did-start-navigation", { isMainFrame: false, isSameDocument: false });
135 expect(port.closed).toBe(false);
136 electron.sender.emit("did-start-navigation", { isMainFrame: true, isSameDocument: true });
137 expect(port.closed).toBe(false);
138 electron.sender.emit("did-start-navigation", { isMainFrame: true, isSameDocument: false });
139 expect(yield* Fiber.join(closing)).toEqual({ type: "close", id: request.id });
140 expect(port.closed).toBe(true);
141 }).pipe(Effect.provide(layer), Effect.scoped),
142 );
143
144 it.effect("rejects embedded frames, other windows and custom-scheme origins", () =>
145 Effect.gen(function* () {
146 const host = yield* DesktopTerminalHost.DesktopTerminalHost;
147 yield* host.configure("http://127.0.0.1:3773");
148 for (const frame of [{ url: "t3code-dev://app/" }, { url: "t3code-dev://evil/" }]) {
149 const port = new electron.Port();
150 electron.ipcMain.emit(DESKTOP_TERMINAL_CHANNEL, event(port, frame), input);
151 expect(port.closed).toBe(true);
152 }
153 const oldUrl = electron.sender.mainFrame.url;
154 electron.sender.mainFrame.url = "t3code-dev://evil/";
155 const navigated = new electron.Port();
156 electron.ipcMain.emit(DESKTOP_TERMINAL_CHANNEL, event(navigated), input);
157 expect(navigated.closed).toBe(true);
158 electron.sender.mainFrame.url = oldUrl;
159 const foreign = new electron.Port();
160 electron.ipcMain.emit(
161 DESKTOP_TERMINAL_CHANNEL,
162 { ...event(foreign), sender: { mainFrame: electron.sender.mainFrame } },
163 input,
164 );
165 expect(foreign.closed).toBe(true);
166 }).pipe(Effect.provide(layer), Effect.scoped),
167 );
168});