1// @effect-diagnostics nodeBuiltinImport:off -- The integration fixture binds the same platform socket or named pipe as the CLI.
2import * as NodeFSP from "node:fs/promises";
3import * as NodeNet from "node:net";
4import * as NodeOS from "node:os";
5import * as NodePath from "node:path";
6
7import * as NodeServices from "@effect/platform-node/NodeServices";
8import { it } from "@effect/vitest";
9import type { DesktopAppActivationRequest } from "@t3tools/contracts";
10import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";
11import * as HostProcess from "@t3tools/shared/HostProcess";
12import * as NetService from "@t3tools/shared/Net";
13import * as ConfigProvider from "effect/ConfigProvider";
14import * as Effect from "effect/Effect";
15import * as Layer from "effect/Layer";
16import * as Schema from "effect/Schema";
17import { Command } from "effect/cli";
18import { describe, expect } from "vite-plus/test";
19
20import { makeCli } from "../binCli.ts";
21import { PersistedServerRuntimeState } from "../serverRuntimeState.ts";
22
23const encodeRuntimeState = Schema.encodeEffect(Schema.fromJsonString(PersistedServerRuntimeState));
24
25const runCli = (args: ReadonlyArray<string>, env: Record<string, string> = {}) =>
26 Command.runWith(makeCli(), { version: "0.0.0" })(args).pipe(
27 Effect.provide(
28 Layer.mergeAll(
29 NodeServices.layer,
30 NetService.layer,
31 ConfigProvider.layer(ConfigProvider.fromEnv({ env })),
32 ),
33 ),
34 );
35
36const pathExists = (path: string) =>
37 Effect.promise(() =>
38 NodeFSP.stat(path).then(
39 () => true,
40 () => false,
41 ),
42 );
43
44async function startFakeDesktop(input: {
45 readonly baseDir: string;
46 readonly stateSubdirectory?: "userdata" | "dev";
47 readonly platform: NodeJS.Platform;
48 readonly userId: number | undefined;
49 readonly reply?: (request: DesktopAppActivationRequest) => unknown;
50}) {
51 const target = resolveDesktopAppControlAddress({
52 stateDir: NodePath.join(input.baseDir, input.stateSubdirectory ?? "userdata"),
53 platform: input.platform,
54 tempDir: NodeOS.tmpdir(),
55 userId: input.userId,
56 joinPath: NodePath.join,
57 });
58 if (target.directory !== null) {
59 await NodeFSP.mkdir(target.directory, { recursive: true, mode: 0o700 });
60 await NodeFSP.unlink(target.address).catch((error: NodeJS.ErrnoException) => {
61 if (error.code !== "ENOENT") throw error;
62 });
63 }
64
65 const received: DesktopAppActivationRequest[] = [];
66 const server = NodeNet.createServer((socket) => {
67 socket.setEncoding("utf8");
68 let buffer = "";
69 socket.on("data", (chunk) => {
70 buffer += chunk;
71 const newline = buffer.indexOf("\n");
72 if (newline === -1) return;
73 const request = JSON.parse(buffer.slice(0, newline)) as DesktopAppActivationRequest;
74 received.push(request);
75 const response = input.reply
76 ? input.reply(request)
77 : {
78 version: 1,
79 requestId: request.requestId,
80 ok: true,
81 projectId: "project-1",
82 threadId: `thread-${received.length}`,
83 };
84 socket.end(`${JSON.stringify(response)}\n`);
85 });
86 });
87 await new Promise<void>((resolve, reject) => {
88 server.once("error", reject);
89 server.listen(target.address, resolve);
90 });
91
92 return {
93 received,
94 close: async () => {
95 await new Promise<void>((resolve) => server.close(() => resolve()));
96 if (target.directory !== null) {
97 await NodeFSP.unlink(target.address).catch((error: NodeJS.ErrnoException) => {
98 if (error.code !== "ENOENT") throw error;
99 });
100 }
101 },
102 };
103}
104
105const fakeDesktop = Effect.fn(function* (
106 input: Omit<Parameters<typeof startFakeDesktop>[0], "platform" | "userId">,
107) {
108 const platform = yield* HostProcess.Platform;
109 const userId = yield* HostProcess.UserId;
110 return yield* Effect.acquireRelease(
111 Effect.promise(() => startFakeDesktop({ ...input, platform, userId })),
112 (server) => Effect.promise(() => server.close()),
113 );
114});
115
116const withTempDirectory = <A, E, R>(
117 prefix: string,
118 use: (root: string) => Effect.Effect<A, E, R>,
119) =>
120 Effect.acquireUseRelease(
121 Effect.promise(() => NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), prefix))),
122 use,
123 (root) => Effect.promise(() => NodeFSP.rm(root, { recursive: true, force: true })),
124 );
125
126describe("t3 server command safety", () => {
127 it.effect("rejects unknown command words without creating a home or project", () =>
128 withTempDirectory("t3-cli-unknown-", (root) =>
129 Effect.gen(function* () {
130 const baseDir = NodePath.join(root, "home");
131 for (const word of [
132 "account",
133 "login",
134 "clients",
135 "conenct",
136 "package.json",
137 "C:new-project",
138 ]) {
139 const error = yield* runCli([word, "--base-dir", baseDir]).pipe(
140 Effect.provideService(HostProcess.Platform, "linux"),
141 Effect.flip,
142 );
143 expect(String(error)).toContain(`Unknown command "${word}"`);
144 expect(yield* pathExists(word)).toBe(word === "package.json");
145 expect(yield* pathExists(baseDir)).toBe(false);
146 }
147 }),
148 ),
149 );
150
151 it.effect("shows help without creating state", () =>
152 withTempDirectory("t3-cli-help-", (root) =>
153 Effect.gen(function* () {
154 const baseDir = NodePath.join(root, "home");
155 const help = yield* runCli(["help"], { T3CODE_HOME: baseDir }).pipe(Effect.flip);
156 expect(help).toMatchObject({ _tag: "ShowHelp", commandPath: ["t3"], errors: [] });
157 expect(yield* pathExists(baseDir)).toBe(false);
158 }),
159 ),
160 );
161
162 it.effect("refuses manual startup over a live server before creating directories", () =>
163 withTempDirectory("t3-cli-running-", (root) =>
164 Effect.gen(function* () {
165 const baseDir = NodePath.join(root, "home");
166 const stateDir = NodePath.join(baseDir, "userdata");
167 const statePath = NodePath.join(stateDir, "server-runtime.json");
168 const record = yield* encodeRuntimeState({
169 version: 1,
170 pid: process.pid,
171 port: 3773,
172 origin: "http://127.0.0.1:3773",
173 startedAt: "2026-10-01T00:00:00.000Z",
174 serviceManaged: true,
175 });
176 yield* Effect.promise(() => NodeFSP.mkdir(stateDir, { recursive: true }));
177 yield* Effect.promise(() => NodeFSP.writeFile(statePath, record));
178 const newDirectory = NodePath.join(root, "new-project");
179 const platform = yield* HostProcess.Platform;
180 for (const args of [
181 [],
182 ["start"],
183 ["."],
184 ["node_modules"],
185 ["C:new-project"],
186 [newDirectory],
187 ["start", newDirectory],
188 ]) {
189 const error = yield* runCli(args, { T3CODE_HOME: baseDir }).pipe(
190 Effect.provideService(
191 HostProcess.Platform,
192 args[0] === "C:new-project" ? "win32" : platform,
193 ),
194 Effect.flip,
195 );
196 expect(String(error)).toContain("A T3 Code server is already running");
197 expect(yield* Effect.promise(() => NodeFSP.readFile(statePath, "utf8"))).toBe(record);
198 expect(yield* pathExists(newDirectory)).toBe(false);
199 expect(yield* Effect.promise(() => NodeFSP.readdir(stateDir))).toEqual([
200 "server-runtime.json",
201 ]);
202 }
203 }),
204 ),
205 );
206});
207
208describe("t3 app", () => {
209 it.effect("rejects SSH before it tries to reach a desktop app", () =>
210 withTempDirectory("t3-app-ssh-test-", (root) =>
211 Effect.gen(function* () {
212 const baseDir = NodePath.join(root, "missing-t3-home");
213 const error = yield* runCli(["app", "--base-dir", baseDir], {
214 SSH_CONNECTION: "client server",
215 }).pipe(Effect.flip);
216
217 expect(error).toMatchObject({
218 _tag: "DesktopAppSshUnsupportedError",
219 message:
220 "`t3 app` only controls a desktop app on the same machine. It cannot run over SSH.",
221 });
222 expect(yield* pathExists(baseDir)).toBe(false);
223 }),
224 ),
225 );
226
227 it.effect("rejects unsupported platforms without creating state", () =>
228 withTempDirectory("t3-app-platform-test-", (root) =>
229 Effect.gen(function* () {
230 const baseDir = NodePath.join(root, "missing-t3-home");
231 const error = yield* runCli(["app", "--base-dir", baseDir]).pipe(
232 Effect.provideService(HostProcess.Platform, "freebsd"),
233 Effect.flip,
234 );
235
236 expect(error).toMatchObject({
237 _tag: "DesktopAppPlatformUnsupportedError",
238 platform: "freebsd",
239 message: "`t3 app` is not supported on freebsd.",
240 });
241 expect(yield* pathExists(baseDir)).toBe(false);
242 }),
243 ),
244 );
245
246 it.effect("does not create state when only a server or no desktop app is running", () =>
247 withTempDirectory("t3-app-missing-test-", (root) =>
248 Effect.gen(function* () {
249 const baseDir = NodePath.join(root, "missing-t3-home");
250 const error = yield* runCli(["app", "--base-dir", baseDir]).pipe(Effect.flip);
251
252 expect(error).toMatchObject({
253 _tag: "DesktopAppUnreachableError",
254 candidateAddresses: [expect.any(String)],
255 workspaceRoot: yield* HostProcess.WorkingDirectory,
256 message: expect.stringContaining("Could not reach the T3 Code desktop app."),
257 cause: { code: "ENOENT" },
258 });
259 expect(yield* pathExists(baseDir)).toBe(false);
260 }),
261 ),
262 );
263
264 it.effect("uses T3CODE_HOME or --base-dir and sends the default or explicit path", () =>
265 withTempDirectory("t3-app-command-test-", (root) =>
266 Effect.gen(function* () {
267 const baseDir = NodePath.join(root, "t3-home");
268 const explicitPath = NodePath.join(root, "project");
269 const platform = yield* HostProcess.Platform;
270 const workingDirectory = yield* HostProcess.WorkingDirectory;
271 const desktop = yield* fakeDesktop({ baseDir });
272
273 yield* runCli(["app"], { T3CODE_HOME: baseDir });
274 yield* runCli(["app", explicitPath, "--base-dir", baseDir]);
275
276 expect(desktop.received.map((request) => request.workspaceRoot)).toEqual([
277 workingDirectory,
278 explicitPath,
279 ]);
280 expect(desktop.received.every((request) => request.platform === platform)).toBe(true);
281 }).pipe(Effect.scoped),
282 ),
283 );
284
285 it.effect("prefers the installed desktop app when a dev desktop is also running", () =>
286 withTempDirectory("t3-app-preferred-test-", (root) =>
287 Effect.gen(function* () {
288 const baseDir = NodePath.join(root, ".t3");
289 const desktop = yield* fakeDesktop({ baseDir });
290 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
291
292 yield* runCli(["app"]);
293
294 expect(desktop.received).toHaveLength(1);
295 expect(development.received).toHaveLength(0);
296 }).pipe(Effect.provideService(HostProcess.HomeDirectory, root), Effect.scoped),
297 ),
298 );
299
300 it.effect("finds the dev desktop when the default desktop socket is absent", () =>
301 withTempDirectory("t3-app-dev-test-", (root) =>
302 Effect.gen(function* () {
303 const baseDir = NodePath.join(root, ".t3");
304 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
305
306 yield* runCli(["app"]);
307 yield* runCli(["app"], { T3CODE_HOME: " " });
308
309 expect(development.received).toHaveLength(2);
310 expect(yield* pathExists(baseDir)).toBe(false);
311 }).pipe(Effect.provideService(HostProcess.HomeDirectory, root), Effect.scoped),
312 ),
313 );
314
315 it.effect("never searches a dev state directory for an explicit T3 home", () =>
316 withTempDirectory("t3-app-explicit-test-", (root) =>
317 Effect.gen(function* () {
318 const baseDir = NodePath.join(root, ".t3");
319 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
320
321 const flagError = yield* runCli(["app", "--base-dir", baseDir]).pipe(Effect.flip);
322 const envError = yield* runCli(["app"], { T3CODE_HOME: baseDir }).pipe(Effect.flip);
323
324 expect(flagError).toMatchObject({ _tag: "DesktopAppUnreachableError" });
325 expect(envError).toMatchObject({ _tag: "DesktopAppUnreachableError" });
326 expect(development.received).toHaveLength(0);
327 }).pipe(Effect.provideService(HostProcess.HomeDirectory, root), Effect.scoped),
328 ),
329 );
330
331 it.effect.each(["failure", "invalid"] as const)(
332 "never falls back after the default desktop sends a %s response",
333 (responseKind) =>
334 withTempDirectory("t3-app-response-test-", (root) =>
335 Effect.gen(function* () {
336 const baseDir = NodePath.join(root, ".t3");
337 const desktop = yield* fakeDesktop({
338 baseDir,
339 reply: (request) =>
340 responseKind === "failure"
341 ? {
342 version: 1,
343 requestId: request.requestId,
344 ok: false,
345 code: "project-create-failed",
346 message: "The project path is not available.",
347 }
348 : { invalid: true },
349 });
350 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
351
352 const error = yield* runCli(["app"]).pipe(Effect.flip);
353
354 expect(desktop.received).toHaveLength(1);
355 expect(development.received).toHaveLength(0);
356 if (responseKind === "failure") {
357 expect(error).toMatchObject({
358 _tag: "DesktopAppRequestFailedError",
359 code: "project-create-failed",
360 requestId: desktop.received[0]?.requestId,
361 workspaceRoot: yield* HostProcess.WorkingDirectory,
362 message: expect.stringContaining("project-create-failed"),
363 cause: {
364 ok: false,
365 code: "project-create-failed",
366 message: "The project path is not available.",
367 },
368 });
369 } else {
370 expect(error).toMatchObject({
371 _tag: "DesktopAppUnreachableError",
372 cause: { message: "The desktop app response is invalid." },
373 });
374 }
375 }).pipe(Effect.provideService(HostProcess.HomeDirectory, root), Effect.scoped),
376 ),
377 );
378});