| author | |
| committer | |
| log | 13a4789a5d2aabd7377fdb15509f17bcc779d84b |
| tree | 3aaad8a391bdca9a22be916546f90934d6c73682 |
| parent | b6e384fb273b56ab9d12237f27684d1392be1692 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 @@ | ... | @@ -1,9 +1,13 @@ |
| 1 | import { expect, it } from "@effect/vitest"; | 1 | import { assert, expect, it } from "@effect/vitest"; |
| 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 3 | import * as Cause from "effect/Cause"; | ||
| 4 | import * as Effect from "effect/Effect"; | ||
| 5 | import * as Exit from "effect/Exit"; | ||
| 2 | 6 | ||
| 3 | import { BunPtyOperationUnavailableError } from "./BunPtyAdapter.ts"; | 7 | import * as BunPtyAdapter from "./BunPtyAdapter.ts"; |
| 4 | 8 | ||
| 5 | it("describes unavailable Bun PTY operations structurally", () => { | 9 | it("describes unavailable Bun PTY operations structurally", () => { |
| 6 | const error = new BunPtyOperationUnavailableError({ | 10 | const error = new BunPtyAdapter.BunPtyOperationUnavailableError({ |
| 7 | operation: "resize", | 11 | operation: "resize", |
| 8 | pid: 42, | 12 | pid: 42, |
| 9 | }); | 13 | }); |
| ... | @@ -15,3 +19,26 @@ it("describes unavailable Bun PTY operations structurally", () => { | ... | @@ -15,3 +19,26 @@ it("describes unavailable Bun PTY operations structurally", () => { |
| 15 | }); | 19 | }); |
| 16 | expect(error.message).toBe("Bun PTY resize is unavailable for process 42."); | 20 | expect(error.message).toBe("Bun PTY resize is unavailable for process 42."); |
| 17 | }); | 21 | }); |
| 22 | |||
| 23 | it.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"; | ... | @@ -7,6 +7,17 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 7 | 7 | ||
| 8 | import * as PtyAdapter from "./PtyAdapter.ts"; | 8 | import * as PtyAdapter from "./PtyAdapter.ts"; |
| 9 | 9 | ||
| 10 | export 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 | |||
| 10 | export class BunPtyOperationUnavailableError extends Schema.TaggedErrorClass<BunPtyOperationUnavailableError>()( | 21 | export class BunPtyOperationUnavailableError extends Schema.TaggedErrorClass<BunPtyOperationUnavailableError>()( |
| 11 | "BunPtyOperationUnavailableError", | 22 | "BunPtyOperationUnavailableError", |
| 12 | { | 23 | { |
| ... | @@ -109,9 +120,7 @@ class BunPtyProcess implements PtyAdapter.PtyProcess { | ... | @@ -109,9 +120,7 @@ class BunPtyProcess implements PtyAdapter.PtyProcess { |
| 109 | export const make = Effect.fn("BunPtyAdapter.make")(function* () { | 120 | export const make = Effect.fn("BunPtyAdapter.make")(function* () { |
| 110 | const platform = yield* HostProcessPlatform; | 121 | const platform = yield* HostProcessPlatform; |
| 111 | if (platform === "win32") { | 122 | if (platform === "win32") { |
| 112 | return yield* Effect.die( | 123 | return yield* Effect.die(new BunPtyUnsupportedPlatformError({ platform })); |
| 113 | "Bun PTY terminal support is unavailable on Windows. Please use Node.js (e.g. by running `npx t3`) instead.", | ||
| 114 | ); | ||
| 115 | } | 124 | } |
| 116 | return PtyAdapter.PtyAdapter.of({ | 125 | return PtyAdapter.PtyAdapter.of({ |
| 117 | spawn: (input) => | 126 | spawn: (input) => |
apps/server/src/terminal/NodePtyAdapter.test.ts+30| ... | @@ -1,7 +1,9 @@ | ... | @@ -1,7 +1,9 @@ |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 3 | import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 3 | import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 4 | import * as Cause from "effect/Cause"; | ||
| 4 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Exit from "effect/Exit"; | ||
| 5 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 6 | import { vi } from "vite-plus/test"; | 8 | import { vi } from "vite-plus/test"; |
| 7 | 9 | ||
| ... | @@ -56,3 +58,31 @@ it.effect("spawns through the public adapter with the provided host references", | ... | @@ -56,3 +58,31 @@ it.effect("spawns through the public adapter with the provided host references", |
| 56 | ]); | 58 | ]); |
| 57 | }).pipe(Effect.provide(testLayer)), | 59 | }).pipe(Effect.provide(testLayer)), |
| 58 | ); | 60 | ); |
| 61 | |||
| 62 | it.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"; | ... | @@ -4,10 +4,26 @@ import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Path from "effect/Path"; | 6 | import * as Path from "effect/Path"; |
| 7 | import * as Schema from "effect/Schema"; | ||
| 7 | import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 8 | import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 8 | 9 | ||
| 9 | import * as PtyAdapter from "./PtyAdapter.ts"; | 10 | import * as PtyAdapter from "./PtyAdapter.ts"; |
| 10 | 11 | ||
| 12 | export 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 | |||
| 25 | type NodePtyModuleLoader = () => Promise<typeof import("node-pty")>; | ||
| 26 | |||
| 11 | let didEnsureSpawnHelperExecutable = false; | 27 | let didEnsureSpawnHelperExecutable = false; |
| 12 | 28 | ||
| 13 | const resolveNodePtySpawnHelperPath = Effect.gen(function* () { | 29 | const resolveNodePtySpawnHelperPath = Effect.gen(function* () { |
| ... | @@ -94,13 +110,23 @@ class NodePtyProcess implements PtyAdapter.PtyProcess { | ... | @@ -94,13 +110,23 @@ class NodePtyProcess implements PtyAdapter.PtyProcess { |
| 94 | } | 110 | } |
| 95 | } | 111 | } |
| 96 | 112 | ||
| 97 | export const make = Effect.fn("NodePtyAdapter.make")(function* () { | 113 | export const make = Effect.fn("NodePtyAdapter.make")(function* ( |
| 114 | loadNodePtyModule: NodePtyModuleLoader = () => import("node-pty"), | ||
| 115 | ) { | ||
| 98 | const fs = yield* FileSystem.FileSystem; | 116 | const fs = yield* FileSystem.FileSystem; |
| 99 | const path = yield* Path.Path; | 117 | const path = yield* Path.Path; |
| 100 | const platform = yield* HostProcessPlatform; | 118 | const platform = yield* HostProcessPlatform; |
| 101 | const architecture = yield* HostProcessArchitecture; | 119 | const architecture = yield* HostProcessArchitecture; |
| 102 | 120 | ||
| 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); | ||
| 104 | 130 | ||
| 105 | const ensureNodePtySpawnHelperExecutableCached = yield* Effect.cached( | 131 | const ensureNodePtySpawnHelperExecutableCached = yield* Effect.cached( |
| 106 | ensureNodePtySpawnHelperExecutable().pipe( | 132 | ensureNodePtySpawnHelperExecutable().pipe( |