authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:40:36-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:40:36-07:00
log13a4789a5d2aabd7377fdb15509f17bcc779d84b
tree3aaad8a391bdca9a22be916546f90934d6c73682
parentb6e384fb273b56ab9d12237f27684d1392be1692
signature Signed by PGP key B5690EEEBB952194

[codex] Structure terminal adapter startup defects (#3425)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 100 insertions(+), 8 deletions(-)

apps/server/src/terminal/BunPtyAdapter.test.ts+30-3
......@@ -1,9 +1,13 @@
1import { expect, it } from "@effect/vitest";
1import { assert, expect, it } from "@effect/vitest";
2import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
3import * as Cause from "effect/Cause";
4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
26
3import { BunPtyOperationUnavailableError } from "./BunPtyAdapter.ts";
7import * as BunPtyAdapter from "./BunPtyAdapter.ts";
48
59it("describes unavailable Bun PTY operations structurally", () => {
6 const error = new BunPtyOperationUnavailableError({
10 const error = new BunPtyAdapter.BunPtyOperationUnavailableError({
711 operation: "resize",
812 pid: 42,
913 });
......@@ -15,3 +19,26 @@ it("describes unavailable Bun PTY operations structurally", () => {
1519 });
1620 expect(error.message).toBe("Bun PTY resize is unavailable for process 42.");
1721});
22
23it.effect("reports unsupported platforms with a structured startup defect", () =>
24 Effect.gen(function* () {
25 const exit = yield* BunPtyAdapter.make().pipe(
26 Effect.provideService(HostProcessPlatform, "win32"),
27 Effect.exit,
28 );
29
30 expect(Exit.isFailure(exit)).toBe(true);
31 if (Exit.isFailure(exit)) {
32 expect(Cause.hasDies(exit.cause)).toBe(true);
33 const error = Cause.squash(exit.cause);
34 assert.instanceOf(error, BunPtyAdapter.BunPtyUnsupportedPlatformError);
35 expect(error).toMatchObject({
36 _tag: "BunPtyUnsupportedPlatformError",
37 platform: "win32",
38 });
39 expect(error.message).toBe(
40 "Bun PTY terminal support is unavailable on win32. Please use Node.js (e.g. by running `npx t3`) instead.",
41 );
42 }
43 }),
44);
apps/server/src/terminal/BunPtyAdapter.ts+12-3
......@@ -7,6 +7,17 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
77
88import * as PtyAdapter from "./PtyAdapter.ts";
99
10export class BunPtyUnsupportedPlatformError extends Schema.TaggedErrorClass<BunPtyUnsupportedPlatformError>()(
11 "BunPtyUnsupportedPlatformError",
12 {
13 platform: Schema.Literal("win32"),
14 },
15) {
16 override get message(): string {
17 return `Bun PTY terminal support is unavailable on ${this.platform}. Please use Node.js (e.g. by running \`npx t3\`) instead.`;
18 }
19}
20
1021export class BunPtyOperationUnavailableError extends Schema.TaggedErrorClass<BunPtyOperationUnavailableError>()(
1122 "BunPtyOperationUnavailableError",
1223 {
......@@ -109,9 +120,7 @@ class BunPtyProcess implements PtyAdapter.PtyProcess {
109120export const make = Effect.fn("BunPtyAdapter.make")(function* () {
110121 const platform = yield* HostProcessPlatform;
111122 if (platform === "win32") {
112 return yield* Effect.die(
113 "Bun PTY terminal support is unavailable on Windows. Please use Node.js (e.g. by running `npx t3`) instead.",
114 );
123 return yield* Effect.die(new BunPtyUnsupportedPlatformError({ platform }));
115124 }
116125 return PtyAdapter.PtyAdapter.of({
117126 spawn: (input) =>
apps/server/src/terminal/NodePtyAdapter.test.ts+30
......@@ -1,7 +1,9 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, it } from "@effect/vitest";
33import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
4import * as Cause from "effect/Cause";
45import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
57import * as Layer from "effect/Layer";
68import { vi } from "vite-plus/test";
79
......@@ -56,3 +58,31 @@ it.effect("spawns through the public adapter with the provided host references",
5658 ]);
5759 }).pipe(Effect.provide(testLayer)),
5860);
61
62it.effect("reports native module load failures as structured startup defects", () =>
63 Effect.gen(function* () {
64 const cause = new Error("native binding could not be loaded");
65 const exit = yield* NodePtyAdapter.make(() => Promise.reject(cause)).pipe(Effect.exit);
66
67 assert.isTrue(Exit.isFailure(exit));
68 if (Exit.isFailure(exit)) {
69 assert.isTrue(Cause.hasDies(exit.cause));
70 const error = Cause.squash(exit.cause);
71 assert.instanceOf(error, NodePtyAdapter.NodePtyModuleLoadError);
72 assert.deepInclude(error, {
73 _tag: "NodePtyModuleLoadError",
74 platform: "win32",
75 architecture: "x64",
76 });
77 assert.equal(error.message, "Failed to load node-pty for win32-x64.");
78 }
79 }).pipe(
80 Effect.provide(
81 Layer.mergeAll(
82 NodeServices.layer,
83 Layer.succeed(HostProcessPlatform, "win32"),
84 Layer.succeed(HostProcessArchitecture, "x64"),
85 ),
86 ),
87 ),
88);
apps/server/src/terminal/NodePtyAdapter.ts+28-2
......@@ -4,10 +4,26 @@ import * as Effect from "effect/Effect";
44import * as FileSystem from "effect/FileSystem";
55import * as Layer from "effect/Layer";
66import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";
78import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
89
910import * as PtyAdapter from "./PtyAdapter.ts";
1011
12export class NodePtyModuleLoadError extends Schema.TaggedErrorClass<NodePtyModuleLoadError>()(
13 "NodePtyModuleLoadError",
14 {
15 platform: Schema.String,
16 architecture: Schema.String,
17 cause: Schema.Defect(),
18 },
19) {
20 override get message(): string {
21 return `Failed to load node-pty for ${this.platform}-${this.architecture}.`;
22 }
23}
24
25type NodePtyModuleLoader = () => Promise<typeof import("node-pty")>;
26
1127let didEnsureSpawnHelperExecutable = false;
1228
1329const resolveNodePtySpawnHelperPath = Effect.gen(function* () {
......@@ -94,13 +110,23 @@ class NodePtyProcess implements PtyAdapter.PtyProcess {
94110 }
95111}
96112
97export const make = Effect.fn("NodePtyAdapter.make")(function* () {
113export const make = Effect.fn("NodePtyAdapter.make")(function* (
114 loadNodePtyModule: NodePtyModuleLoader = () => import("node-pty"),
115) {
98116 const fs = yield* FileSystem.FileSystem;
99117 const path = yield* Path.Path;
100118 const platform = yield* HostProcessPlatform;
101119 const architecture = yield* HostProcessArchitecture;
102120
103 const nodePty = yield* Effect.promise(() => import("node-pty"));
121 const nodePty = yield* Effect.tryPromise({
122 try: loadNodePtyModule,
123 catch: (cause) =>
124 new NodePtyModuleLoadError({
125 platform,
126 architecture,
127 cause,
128 }),
129 }).pipe(Effect.orDie);
104130
105131 const ensureNodePtySpawnHelperExecutableCached = yield* Effect.cached(
106132 ensureNodePtySpawnHelperExecutable().pipe(