authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:28:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:28:33-07:00
log833c8ab7c66f4e976c9ae0641d5b1d2780348734
tree9a159bf4295404cc1d351030fdc0ebeeb9b2c436
parent5b1b35c773e8ad4a85ea2f3dd277b698bf329f21
signature Signed by PGP key B5690EEEBB952194

[codex] Remove project setup error constructor wrappers (#3329)

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

2 files changed, 101 insertions(+), 41 deletions(-)

apps/server/src/project/ProjectSetupScriptRunner.test.ts+51
...@@ -3,11 +3,16 @@ import { type OrchestrationProject, ProjectId } from "@t3tools/contracts";...@@ -3,11 +3,16 @@ import { type OrchestrationProject, ProjectId } from "@t3tools/contracts";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
67
7import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";8import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
8import * as TerminalManager from "../terminal/Manager.ts";9import * as TerminalManager from "../terminal/Manager.ts";
9import * as ProjectSetupScriptRunner from "./ProjectSetupScriptRunner.ts";10import * as ProjectSetupScriptRunner from "./ProjectSetupScriptRunner.ts";
1011
12const isProjectSetupScriptOperationError = Schema.is(
13 ProjectSetupScriptRunner.ProjectSetupScriptOperationError,
14);
15
11const makeProject = (scripts: OrchestrationProject["scripts"]): OrchestrationProject => ({16const makeProject = (scripts: OrchestrationProject["scripts"]): OrchestrationProject => ({
12 id: ProjectId.make("project-1"),17 id: ProjectId.make("project-1"),
13 title: "Project",18 title: "Project",
...@@ -145,4 +150,50 @@ describe("ProjectSetupScriptRunner", () => {...@@ -145,4 +150,50 @@ describe("ProjectSetupScriptRunner", () => {
145 }).pipe(Effect.provide(testLayer(project, { open, write })));150 }).pipe(Effect.provide(testLayer(project, { open, write })));
146 },151 },
147 );152 );
153
154 it.effect("keeps terminal failures as the exact cause of a structured operation error", () => {
155 const rootCause = new Error("stat failed");
156 const terminalError = new TerminalManager.TerminalCwdError({
157 cwd: "/repo/worktrees/a",
158 reason: "statFailed",
159 cause: rootCause,
160 });
161 const project = makeProject([
162 {
163 id: "setup",
164 name: "Setup",
165 command: "bun install",
166 icon: "configure",
167 runOnWorktreeCreate: true,
168 },
169 ]);
170
171 return Effect.gen(function* () {
172 const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
173 const error = yield* runner
174 .runForThread({
175 threadId: "thread-1",
176 projectId: "project-1",
177 worktreePath: "/repo/worktrees/a",
178 })
179 .pipe(Effect.flip);
180
181 expect(isProjectSetupScriptOperationError(error)).toBe(true);
182 if (isProjectSetupScriptOperationError(error)) {
183 expect(error.operation).toBe("openTerminal");
184 expect(error.threadId).toBe("thread-1");
185 expect(error.projectId).toBe("project-1");
186 expect(error.worktreePath).toBe("/repo/worktrees/a");
187 expect(error.cause).toBe(terminalError);
188 expect(terminalError.cause).toBe(rootCause);
189 }
190 }).pipe(
191 Effect.provide(
192 testLayer(project, {
193 open: () => Effect.fail(terminalError),
194 write: () => Effect.die("unexpected write"),
195 }),
196 ),
197 );
198 });
148});199});
apps/server/src/project/ProjectSetupScriptRunner.ts+50-41
...@@ -59,7 +59,7 @@ export class ProjectSetupScriptProjectNotFoundError extends Schema.TaggedErrorCl...@@ -59,7 +59,7 @@ export class ProjectSetupScriptProjectNotFoundError extends Schema.TaggedErrorCl
59 },59 },
60) {60) {
61 override get message(): string {61 override get message(): string {
62 return "Project was not found for setup script execution.";62 return `Project was not found for setup script execution for thread '${this.threadId}' in '${this.worktreePath}'.`;
63 }63 }
64}64}
6565
...@@ -78,32 +78,6 @@ export class ProjectSetupScriptRunner extends Context.Service<...@@ -78,32 +78,6 @@ export class ProjectSetupScriptRunner extends Context.Service<
78 }78 }
79>()("t3/project/ProjectSetupScriptRunner") {}79>()("t3/project/ProjectSetupScriptRunner") {}
8080
81const isProjectSetupScriptRunnerError = Schema.is(ProjectSetupScriptRunnerError);
82
83function operationError(
84 input: ProjectSetupScriptRunnerInput,
85 operation: ProjectSetupScriptOperationError["operation"],
86 cause: unknown,
87): ProjectSetupScriptOperationError {
88 return new ProjectSetupScriptOperationError({
89 threadId: input.threadId,
90 worktreePath: input.worktreePath,
91 operation,
92 cause,
93 ...(input.projectId === undefined ? {} : { projectId: input.projectId }),
94 ...(input.projectCwd === undefined ? {} : { projectCwd: input.projectCwd }),
95 });
96}
97
98function mapRunnerError(
99 input: ProjectSetupScriptRunnerInput,
100 operation: ProjectSetupScriptOperationError["operation"],
101) {
102 return Effect.mapError((cause: unknown) =>
103 isProjectSetupScriptRunnerError(cause) ? cause : operationError(input, operation, cause),
104 );
105}
106
107export const make = Effect.gen(function* () {81export const make = Effect.gen(function* () {
108 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;82 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
109 const terminalManager = yield* TerminalManager.TerminalManager;83 const terminalManager = yield* TerminalManager.TerminalManager;
...@@ -111,26 +85,43 @@ export const make = Effect.gen(function* () {...@@ -111,26 +85,43 @@ export const make = Effect.gen(function* () {
111 const runForThread: ProjectSetupScriptRunner["Service"]["runForThread"] = Effect.fn(85 const runForThread: ProjectSetupScriptRunner["Service"]["runForThread"] = Effect.fn(
112 "ProjectSetupScriptRunner.runForThread",86 "ProjectSetupScriptRunner.runForThread",
113 )(function* (input) {87 )(function* (input) {
88 const errorContext = {
89 threadId: input.threadId,
90 worktreePath: input.worktreePath,
91 ...(input.projectId === undefined ? {} : { projectId: input.projectId }),
92 ...(input.projectCwd === undefined ? {} : { projectCwd: input.projectCwd }),
93 };
114 const projectById = input.projectId94 const projectById = input.projectId
115 ? yield* projectionSnapshotQuery95 ? yield* projectionSnapshotQuery.getProjectShellById(ProjectId.make(input.projectId)).pipe(
116 .getProjectShellById(ProjectId.make(input.projectId))96 Effect.map(Option.getOrUndefined),
117 .pipe(Effect.map(Option.getOrUndefined), mapRunnerError(input, "resolveProject"))97 Effect.mapError(
98 (cause) =>
99 new ProjectSetupScriptOperationError({
100 ...errorContext,
101 operation: "resolveProject",
102 cause,
103 }),
104 ),
105 )
118 : null;106 : null;
119 const project =107 const project =
120 projectById ??108 projectById ??
121 (input.projectCwd109 (input.projectCwd
122 ? yield* projectionSnapshotQuery110 ? yield* projectionSnapshotQuery.getActiveProjectByWorkspaceRoot(input.projectCwd).pipe(
123 .getActiveProjectByWorkspaceRoot(input.projectCwd)111 Effect.map(Option.getOrUndefined),
124 .pipe(Effect.map(Option.getOrUndefined), mapRunnerError(input, "resolveProject"))112 Effect.mapError(
113 (cause) =>
114 new ProjectSetupScriptOperationError({
115 ...errorContext,
116 operation: "resolveProject",
117 cause,
118 }),
119 ),
120 )
125 : null);121 : null);
126122
127 if (!project) {123 if (!project) {
128 return yield* new ProjectSetupScriptProjectNotFoundError({124 return yield* new ProjectSetupScriptProjectNotFoundError(errorContext);
129 threadId: input.threadId,
130 worktreePath: input.worktreePath,
131 ...(input.projectId === undefined ? {} : { projectId: input.projectId }),
132 ...(input.projectCwd === undefined ? {} : { projectCwd: input.projectCwd }),
133 });
134 }125 }
135126
136 const script = setupProjectScript(project.scripts);127 const script = setupProjectScript(project.scripts);
...@@ -155,14 +146,32 @@ export const make = Effect.gen(function* () {...@@ -155,14 +146,32 @@ export const make = Effect.gen(function* () {
155 worktreePath: input.worktreePath,146 worktreePath: input.worktreePath,
156 env,147 env,
157 })148 })
158 .pipe(mapRunnerError(input, "openTerminal"));149 .pipe(
150 Effect.mapError(
151 (cause) =>
152 new ProjectSetupScriptOperationError({
153 ...errorContext,
154 operation: "openTerminal",
155 cause,
156 }),
157 ),
158 );
159 yield* terminalManager159 yield* terminalManager
160 .write({160 .write({
161 threadId: input.threadId,161 threadId: input.threadId,
162 terminalId,162 terminalId,
163 data: `${script.command}\r`,163 data: `${script.command}\r`,
164 })164 })
165 .pipe(mapRunnerError(input, "writeCommand"));165 .pipe(
166 Effect.mapError(
167 (cause) =>
168 new ProjectSetupScriptOperationError({
169 ...errorContext,
170 operation: "writeCommand",
171 cause,
172 }),
173 ),
174 );
166175
167 return {176 return {
168 status: "started",177 status: "started",