From 5681b5c2c971bb4238c97c94469f9ae80a899c8b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 11:28:19 -0700 Subject: [PATCH] [codex] Structure terminal adapter startup defects Co-authored-by: codex --- .../server/src/terminal/BunPtyAdapter.test.ts | 33 +++++++++++++++++-- apps/server/src/terminal/BunPtyAdapter.ts | 15 +++++++-- .../src/terminal/NodePtyAdapter.test.ts | 30 +++++++++++++++++ apps/server/src/terminal/NodePtyAdapter.ts | 30 +++++++++++++++-- 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/apps/server/src/terminal/BunPtyAdapter.test.ts b/apps/server/src/terminal/BunPtyAdapter.test.ts index 39e811db3a9..e04a54e6d33 100644 --- a/apps/server/src/terminal/BunPtyAdapter.test.ts +++ b/apps/server/src/terminal/BunPtyAdapter.test.ts @@ -1,9 +1,13 @@ -import { expect, it } from "@effect/vitest"; +import { assert, expect, it } from "@effect/vitest"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; -import { BunPtyOperationUnavailableError } from "./BunPtyAdapter.ts"; +import * as BunPtyAdapter from "./BunPtyAdapter.ts"; it("describes unavailable Bun PTY operations structurally", () => { - const error = new BunPtyOperationUnavailableError({ + const error = new BunPtyAdapter.BunPtyOperationUnavailableError({ operation: "resize", pid: 42, }); @@ -15,3 +19,26 @@ it("describes unavailable Bun PTY operations structurally", () => { }); expect(error.message).toBe("Bun PTY resize is unavailable for process 42."); }); + +it.effect("reports unsupported platforms with a structured startup defect", () => + Effect.gen(function* () { + const exit = yield* BunPtyAdapter.make().pipe( + Effect.provideService(HostProcessPlatform, "win32"), + Effect.exit, + ); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.hasDies(exit.cause)).toBe(true); + const error = Cause.squash(exit.cause); + assert.instanceOf(error, BunPtyAdapter.BunPtyUnsupportedPlatformError); + expect(error).toMatchObject({ + _tag: "BunPtyUnsupportedPlatformError", + platform: "win32", + }); + expect(error.message).toBe( + "Bun PTY terminal support is unavailable on win32. Please use Node.js (e.g. by running `npx t3`) instead.", + ); + } + }), +); diff --git a/apps/server/src/terminal/BunPtyAdapter.ts b/apps/server/src/terminal/BunPtyAdapter.ts index 5d7a44a1071..88b68940de1 100644 --- a/apps/server/src/terminal/BunPtyAdapter.ts +++ b/apps/server/src/terminal/BunPtyAdapter.ts @@ -7,6 +7,17 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as PtyAdapter from "./PtyAdapter.ts"; +export class BunPtyUnsupportedPlatformError extends Schema.TaggedErrorClass()( + "BunPtyUnsupportedPlatformError", + { + platform: Schema.Literal("win32"), + }, +) { + override get message(): string { + return `Bun PTY terminal support is unavailable on ${this.platform}. Please use Node.js (e.g. by running \`npx t3\`) instead.`; + } +} + export class BunPtyOperationUnavailableError extends Schema.TaggedErrorClass()( "BunPtyOperationUnavailableError", { @@ -109,9 +120,7 @@ class BunPtyProcess implements PtyAdapter.PtyProcess { export const make = Effect.fn("BunPtyAdapter.make")(function* () { const platform = yield* HostProcessPlatform; if (platform === "win32") { - return yield* Effect.die( - "Bun PTY terminal support is unavailable on Windows. Please use Node.js (e.g. by running `npx t3`) instead.", - ); + return yield* Effect.die(new BunPtyUnsupportedPlatformError({ platform })); } return PtyAdapter.PtyAdapter.of({ spawn: (input) => diff --git a/apps/server/src/terminal/NodePtyAdapter.test.ts b/apps/server/src/terminal/NodePtyAdapter.test.ts index 798e96e3a26..ed87440d499 100644 --- a/apps/server/src/terminal/NodePtyAdapter.test.ts +++ b/apps/server/src/terminal/NodePtyAdapter.test.ts @@ -1,7 +1,9 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import { vi } from "vite-plus/test"; @@ -56,3 +58,31 @@ it.effect("spawns through the public adapter with the provided host references", ]); }).pipe(Effect.provide(testLayer)), ); + +it.effect("reports native module load failures as structured startup defects", () => + Effect.gen(function* () { + const cause = new Error("native binding could not be loaded"); + const exit = yield* NodePtyAdapter.make(() => Promise.reject(cause)).pipe(Effect.exit); + + assert.isTrue(Exit.isFailure(exit)); + if (Exit.isFailure(exit)) { + assert.isTrue(Cause.hasDies(exit.cause)); + const error = Cause.squash(exit.cause); + assert.instanceOf(error, NodePtyAdapter.NodePtyModuleLoadError); + assert.deepInclude(error, { + _tag: "NodePtyModuleLoadError", + platform: "win32", + architecture: "x64", + }); + assert.equal(error.message, "Failed to load node-pty for win32-x64."); + } + }).pipe( + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + Layer.succeed(HostProcessPlatform, "win32"), + Layer.succeed(HostProcessArchitecture, "x64"), + ), + ), + ), +); diff --git a/apps/server/src/terminal/NodePtyAdapter.ts b/apps/server/src/terminal/NodePtyAdapter.ts index 7518901bfdd..ac06e1edfab 100644 --- a/apps/server/src/terminal/NodePtyAdapter.ts +++ b/apps/server/src/terminal/NodePtyAdapter.ts @@ -4,10 +4,26 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as PtyAdapter from "./PtyAdapter.ts"; +export class NodePtyModuleLoadError extends Schema.TaggedErrorClass()( + "NodePtyModuleLoadError", + { + platform: Schema.String, + architecture: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to load node-pty for ${this.platform}-${this.architecture}.`; + } +} + +type NodePtyModuleLoader = () => Promise; + let didEnsureSpawnHelperExecutable = false; const resolveNodePtySpawnHelperPath = Effect.gen(function* () { @@ -94,13 +110,23 @@ class NodePtyProcess implements PtyAdapter.PtyProcess { } } -export const make = Effect.fn("NodePtyAdapter.make")(function* () { +export const make = Effect.fn("NodePtyAdapter.make")(function* ( + loadNodePtyModule: NodePtyModuleLoader = () => import("node-pty"), +) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const platform = yield* HostProcessPlatform; const architecture = yield* HostProcessArchitecture; - const nodePty = yield* Effect.promise(() => import("node-pty")); + const nodePty = yield* Effect.tryPromise({ + try: loadNodePtyModule, + catch: (cause) => + new NodePtyModuleLoadError({ + platform, + architecture, + cause, + }), + }).pipe(Effect.orDie); const ensureNodePtySpawnHelperExecutableCached = yield* Effect.cached( ensureNodePtySpawnHelperExecutable().pipe(