authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 18:05:12-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 19:27:08-08:00
loga2330938d6f3c2090f4af3dfc964efceca8956a3
tree474948d6d8b06d0964ce650059f24a7c86049d5c
parent5aa517863a810010984f9c25a8acfd63f6322e65

Normalize Git and PTY adapter errors across services

- Use domain errors directly in GitManager tests (`GitHubCliError`, `TextGenerationError`) - Remove fixture-only GH error normalization in test scaffolding - Introduce shared `PtyAdapterInitializationError` and use it in NodePTY adapter init

2 files changed, 26 insertions(+), 52 deletions(-)

apps/server/src/git/Layers/GitManager.test.ts+17-41
...@@ -3,7 +3,7 @@ import path from "node:path";...@@ -3,7 +3,7 @@ import path from "node:path";
33
4import * as NodeServices from "@effect/platform-node/NodeServices";4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { it } from "@effect/vitest";5import { it } from "@effect/vitest";
6import { Effect, FileSystem, Layer, PlatformError, Schema, Scope } from "effect";6import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
7import { expect } from "vitest";7import { expect } from "vitest";
88
9import { GitCommandError, GitHubCliError, TextGenerationError } from "../Errors.ts";9import { GitCommandError, GitHubCliError, TextGenerationError } from "../Errors.ts";
...@@ -19,16 +19,11 @@ import { GitService } from "../Services/GitService.ts";...@@ -19,16 +19,11 @@ import { GitService } from "../Services/GitService.ts";
19import { GitCoreLive } from "./GitCore.ts";19import { GitCoreLive } from "./GitCore.ts";
20import { makeGitManager } from "./GitManager.ts";20import { makeGitManager } from "./GitManager.ts";
2121
22class GitFixtureError extends Schema.TaggedErrorClass<GitFixtureError>()("GitFixtureError", {
23 message: Schema.String,
24 cause: Schema.optional(Schema.Defect),
25}) {}
26
27interface FakeGhScenario {22interface FakeGhScenario {
28 prListSequence?: string[];23 prListSequence?: string[];
29 createdPrUrl?: string;24 createdPrUrl?: string;
30 defaultBranch?: string;25 defaultBranch?: string;
31 failWith?: GitFixtureError;26 failWith?: GitHubCliError;
32}27}
3328
34interface FakeGitTextGeneration {29interface FakeGitTextGeneration {
...@@ -37,7 +32,7 @@ interface FakeGitTextGeneration {...@@ -37,7 +32,7 @@ interface FakeGitTextGeneration {
37 branch: string | null;32 branch: string | null;
38 stagedSummary: string;33 stagedSummary: string;
39 stagedPatch: string;34 stagedPatch: string;
40 }) => Effect.Effect<{ subject: string; body: string }, GitFixtureError>;35 }) => Effect.Effect<{ subject: string; body: string }, TextGenerationError>;
41 generatePrContent: (input: {36 generatePrContent: (input: {
42 cwd: string;37 cwd: string;
43 baseBranch: string;38 baseBranch: string;
...@@ -45,7 +40,7 @@ interface FakeGitTextGeneration {...@@ -45,7 +40,7 @@ interface FakeGitTextGeneration {
45 commitSummary: string;40 commitSummary: string;
46 diffSummary: string;41 diffSummary: string;
47 diffPatch: string;42 diffPatch: string;
48 }) => Effect.Effect<{ title: string; body: string }, GitFixtureError>;43 }) => Effect.Effect<{ title: string; body: string }, TextGenerationError>;
49}44}
5045
51function makeTempDir(46function makeTempDir(
...@@ -144,34 +139,6 @@ function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): T...@@ -144,34 +139,6 @@ function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): T
144 };139 };
145}140}
146141
147const normalizeGhError = (error: GitFixtureError): GitHubCliError => {
148 if (error.message.includes("Command not found: gh")) {
149 return new GitHubCliError({
150 operation: "execute",
151 detail: "GitHub CLI (`gh`) is required but not available on PATH.",
152 cause: error,
153 });
154 }
155 const lower = error.message.toLowerCase();
156 if (
157 lower.includes("authentication failed") ||
158 lower.includes("not logged in") ||
159 lower.includes("gh auth login") ||
160 lower.includes("no oauth token")
161 ) {
162 return new GitHubCliError({
163 operation: "execute",
164 detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.",
165 cause: error,
166 });
167 }
168 return new GitHubCliError({
169 operation: "execute",
170 detail: error.message,
171 cause: error,
172 });
173};
174
175function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {142function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
176 service: GitHubCliShape;143 service: GitHubCliShape;
177 ghCalls: string[];144 ghCalls: string[];
...@@ -184,7 +151,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {...@@ -184,7 +151,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
184 ghCalls.push(args.join(" "));151 ghCalls.push(args.join(" "));
185152
186 if (scenario.failWith) {153 if (scenario.failWith) {
187 return Effect.fail(normalizeGhError(scenario.failWith));154 return Effect.fail(scenario.failWith);
188 }155 }
189156
190 if (args[0] === "pr" && args[1] === "list") {157 if (args[0] === "pr" && args[1] === "list") {
...@@ -378,7 +345,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -378,7 +345,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
378345
379 const { manager } = yield* makeManager({346 const { manager } = yield* makeManager({
380 ghScenario: {347 ghScenario: {
381 failWith: new GitFixtureError({ message: "Command not found: gh" }),348 failWith: new GitHubCliError({
349 operation: "execute",
350 detail: "GitHub CLI (`gh`) is required but not available on PATH.",
351 }),
382 },352 },
383 });353 });
384354
...@@ -622,7 +592,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -622,7 +592,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
622592
623 const { manager } = yield* makeManager({593 const { manager } = yield* makeManager({
624 ghScenario: {594 ghScenario: {
625 failWith: new GitFixtureError({ message: "Command not found: gh" }),595 failWith: new GitHubCliError({
596 operation: "execute",
597 detail: "GitHub CLI (`gh`) is required but not available on PATH.",
598 }),
626 },599 },
627 });600 });
628601
...@@ -648,7 +621,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -648,7 +621,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
648621
649 const { manager } = yield* makeManager({622 const { manager } = yield* makeManager({
650 ghScenario: {623 ghScenario: {
651 failWith: new GitFixtureError({ message: "authentication failed" }),624 failWith: new GitHubCliError({
625 operation: "execute",
626 detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.",
627 }),
652 },628 },
653 });629 });
654630
apps/server/src/terminal/Layers/NodePTY.ts+9-11
...@@ -1,18 +1,16 @@...@@ -1,18 +1,16 @@
1import { createRequire } from "node:module";1import { createRequire } from "node:module";
22
3import { Effect, FileSystem, Layer, Path, Schema } from "effect";3import { Effect, FileSystem, Layer, Path } from "effect";
4import { PtyAdapter, PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY";4import {
5 PtyAdapter,
6 PtyAdapterInitializationError,
7 PtyAdapterShape,
8 PtyExitEvent,
9 PtyProcess,
10} from "../Services/PTY";
511
6let didEnsureSpawnHelperExecutable = false;12let didEnsureSpawnHelperExecutable = false;
713
8export class NodePtyAdapterError extends Schema.TaggedErrorClass<NodePtyAdapterError>()(
9 "NodePtyAdapterError",
10 {
11 message: Schema.String,
12 cause: Schema.optional(Schema.Defect),
13 },
14) {}
15
16const resolveNodePtySpawnHelperPath = Effect.gen(function* () {14const resolveNodePtySpawnHelperPath = Effect.gen(function* () {
17 const requireForNodePty = createRequire(import.meta.url);15 const requireForNodePty = createRequire(import.meta.url);
18 const path = yield* Path.Path;16 const path = yield* Path.Path;
...@@ -101,7 +99,7 @@ export const NodePtyAdapterLive = Layer.effect(...@@ -101,7 +99,7 @@ export const NodePtyAdapterLive = Layer.effect(
101 const nodePty = yield* Effect.tryPromise({99 const nodePty = yield* Effect.tryPromise({
102 try: () => import("node-pty"),100 try: () => import("node-pty"),
103 catch: (cause) =>101 catch: (cause) =>
104 new NodePtyAdapterError({102 new PtyAdapterInitializationError({
105 message: "failed to load node-pty module",103 message: "failed to load node-pty module",
106 cause,104 cause,
107 }),105 }),