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";
33
44import * as NodeServices from "@effect/platform-node/NodeServices";
55import { it } from "@effect/vitest";
6import { Effect, FileSystem, Layer, PlatformError, Schema, Scope } from "effect";
6import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
77import { expect } from "vitest";
88
99import { GitCommandError, GitHubCliError, TextGenerationError } from "../Errors.ts";
......@@ -19,16 +19,11 @@ import { GitService } from "../Services/GitService.ts";
1919import { GitCoreLive } from "./GitCore.ts";
2020import { makeGitManager } from "./GitManager.ts";
2121
22class GitFixtureError extends Schema.TaggedErrorClass<GitFixtureError>()("GitFixtureError", {
23 message: Schema.String,
24 cause: Schema.optional(Schema.Defect),
25}) {}
26
2722interface FakeGhScenario {
2823 prListSequence?: string[];
2924 createdPrUrl?: string;
3025 defaultBranch?: string;
31 failWith?: GitFixtureError;
26 failWith?: GitHubCliError;
3227}
3328
3429interface FakeGitTextGeneration {
......@@ -37,7 +32,7 @@ interface FakeGitTextGeneration {
3732 branch: string | null;
3833 stagedSummary: string;
3934 stagedPatch: string;
40 }) => Effect.Effect<{ subject: string; body: string }, GitFixtureError>;
35 }) => Effect.Effect<{ subject: string; body: string }, TextGenerationError>;
4136 generatePrContent: (input: {
4237 cwd: string;
4338 baseBranch: string;
......@@ -45,7 +40,7 @@ interface FakeGitTextGeneration {
4540 commitSummary: string;
4641 diffSummary: string;
4742 diffPatch: string;
48 }) => Effect.Effect<{ title: string; body: string }, GitFixtureError>;
43 }) => Effect.Effect<{ title: string; body: string }, TextGenerationError>;
4944}
5045
5146function makeTempDir(
......@@ -144,34 +139,6 @@ function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): T
144139 };
145140}
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
175142function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
176143 service: GitHubCliShape;
177144 ghCalls: string[];
......@@ -184,7 +151,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
184151 ghCalls.push(args.join(" "));
185152
186153 if (scenario.failWith) {
187 return Effect.fail(normalizeGhError(scenario.failWith));
154 return Effect.fail(scenario.failWith);
188155 }
189156
190157 if (args[0] === "pr" && args[1] === "list") {
......@@ -378,7 +345,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
378345
379346 const { manager } = yield* makeManager({
380347 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 }),
382352 },
383353 });
384354
......@@ -622,7 +592,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
622592
623593 const { manager } = yield* makeManager({
624594 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 }),
626599 },
627600 });
628601
......@@ -648,7 +621,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
648621
649622 const { manager } = yield* makeManager({
650623 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 }),
652628 },
653629 });
654630
apps/server/src/terminal/Layers/NodePTY.ts+9-11
......@@ -1,18 +1,16 @@
11import { createRequire } from "node:module";
22
3import { Effect, FileSystem, Layer, Path, Schema } from "effect";
4import { PtyAdapter, PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY";
3import { Effect, FileSystem, Layer, Path } from "effect";
4import {
5 PtyAdapter,
6 PtyAdapterInitializationError,
7 PtyAdapterShape,
8 PtyExitEvent,
9 PtyProcess,
10} from "../Services/PTY";
511
612let 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
1614const resolveNodePtySpawnHelperPath = Effect.gen(function* () {
1715 const requireForNodePty = createRequire(import.meta.url);
1816 const path = yield* Path.Path;
......@@ -101,7 +99,7 @@ export const NodePtyAdapterLive = Layer.effect(
10199 const nodePty = yield* Effect.tryPromise({
102100 try: () => import("node-pty"),
103101 catch: (cause) =>
104 new NodePtyAdapterError({
102 new PtyAdapterInitializationError({
105103 message: "failed to load node-pty module",
106104 cause,
107105 }),