authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:56:46-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:56:46-07:00
loga26f0dc89fd46f88529823511cdd068c9cd2c151
treecf971f463c0cd8c122fdfccd23ffdf826e16356b
parent6ff6c13fc8bff69e2967a4f768a076bbb1b88aa5
signature Signed by PGP key B5690EEEBB952194

[codex] structure source-control repository failures (#3336)

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

2 files changed, 98 insertions(+), 45 deletions(-)

apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+83-4
......@@ -1,11 +1,13 @@
1import * as NodePath from "@effect/platform-node/NodePath";
12import * as NodeServices from "@effect/platform-node/NodeServices";
23import { assert, it } from "@effect/vitest";
34import * as Effect from "effect/Effect";
45import * as FileSystem from "effect/FileSystem";
56import * as Layer from "effect/Layer";
7import * as PlatformError from "effect/PlatformError";
68import { ChildProcessSpawner } from "effect/unstable/process";
79
8import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts";
10import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts";
911
1012import * as ServerConfig from "../config.ts";
1113import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
......@@ -54,8 +56,9 @@ function processOutput(): GitVcsDriver.ExecuteGitResult {
5456function makeLayer(input: {
5557 readonly provider?: SourceControlProvider.SourceControlProvider["Service"];
5658 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
59 readonly fileSystem?: FileSystem.FileSystem;
5760}) {
58 return SourceControlRepositoryService.layer.pipe(
61 const serviceLayer = SourceControlRepositoryService.layer.pipe(
5962 Layer.provide(
6063 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
6164 get: () => Effect.succeed(input.provider ?? makeProvider()),
......@@ -75,9 +78,20 @@ function makeLayer(input: {
7578 ...input.git,
7679 }),
7780 ),
78 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-repos-" })),
79 Layer.provideMerge(NodeServices.layer),
81 Layer.provide(
82 ServerConfig.layerTest(
83 process.cwd(),
84 input.fileSystem ? "/tmp/t3-source-control-repos" : { prefix: "t3-source-control-repos-" },
85 ),
86 ),
8087 );
88
89 return input.fileSystem
90 ? serviceLayer.pipe(
91 Layer.provide(Layer.succeed(FileSystem.FileSystem, input.fileSystem)),
92 Layer.provideMerge(NodePath.layer),
93 )
94 : serviceLayer.pipe(Layer.provideMerge(NodeServices.layer));
8195}
8296
8397it.effect("looks up repositories through the requested provider without search", () => {
......@@ -103,6 +117,39 @@ it.effect("looks up repositories through the requested provider without search",
103117 }).pipe(Effect.provide(makeLayer({ provider })));
104118});
105119
120it.effect("preserves provider failures without deriving the repository message from them", () => {
121 const providerCause = new SourceControlProviderError({
122 provider: "github",
123 operation: "getRepositoryCloneUrls",
124 cwd: "/workspace",
125 repository: "octocat/t3code",
126 detail: "credential token abc123 was rejected",
127 });
128 const provider = makeProvider({
129 getRepositoryCloneUrls: () => Effect.fail(providerCause),
130 });
131
132 return Effect.gen(function* () {
133 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
134 const error = yield* Effect.flip(
135 service.lookupRepository({
136 provider: "github",
137 repository: "octocat/t3code",
138 cwd: "/workspace",
139 }),
140 );
141
142 assert.strictEqual(error.provider, "github");
143 assert.strictEqual(error.operation, "lookupRepository");
144 assert.strictEqual(error.detail, "The source control operation could not be completed.");
145 assert.strictEqual(
146 error.message,
147 "Source control repository operation lookupRepository failed for github: The source control operation could not be completed.",
148 );
149 assert.strictEqual(error.cause, providerCause);
150 }).pipe(Effect.provide(makeLayer({ provider })));
151});
152
106153it.effect("clones a looked-up repository into the requested destination", () =>
107154 Effect.gen(function* () {
108155 const fs = yield* FileSystem.FileSystem;
......@@ -148,6 +195,38 @@ it.effect("clones a looked-up repository into the requested destination", () =>
148195 }).pipe(Effect.provide(NodeServices.layer)),
149196);
150197
198it.effect("preserves destination probe failures instead of treating them as missing paths", () => {
199 const fileSystemCause = PlatformError.systemError({
200 _tag: "PermissionDenied",
201 module: "FileSystem",
202 method: "exists",
203 pathOrDescriptor: "/restricted/t3code",
204 });
205
206 return Effect.gen(function* () {
207 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
208 const error = yield* Effect.flip(
209 service.cloneRepository({
210 remoteUrl: CLONE_URLS.sshUrl,
211 destinationPath: "/restricted/t3code",
212 }),
213 );
214
215 assert.strictEqual(error.provider, "unknown");
216 assert.strictEqual(error.operation, "cloneRepository");
217 assert.strictEqual(error.cause, fileSystemCause);
218 }).pipe(
219 Effect.provide(
220 makeLayer({
221 fileSystem: FileSystem.makeNoop({
222 exists: () => Effect.fail(fileSystemCause),
223 makeDirectory: () => Effect.void,
224 }),
225 }),
226 ),
227 );
228});
229
151230it.effect("publishes by creating the repository, adding a remote, and pushing upstream", () => {
152231 const createCalls: Array<{ cwd: string; repository: string; visibility: string }> = [];
153232 const remoteCalls: Array<{ cwd: string; preferredName: string; url: string }> = [];
apps/server/src/sourceControl/SourceControlRepositoryService.ts+15-41
......@@ -39,41 +39,14 @@ export class SourceControlRepositoryService extends Context.Service<
3939 }
4040>()("t3/sourceControl/SourceControlRepositoryService") {}
4141
42function detailFromUnknown(cause: unknown): string {
43 if (typeof cause === "object" && cause !== null) {
44 if ("detail" in cause && typeof cause.detail === "string" && cause.detail.length > 0) {
45 return cause.detail;
46 }
47 if ("message" in cause && typeof cause.message === "string" && cause.message.length > 0) {
48 return cause.message;
49 }
50 }
51
52 return "An unexpected source control error occurred.";
53}
54
55function repositoryError(input: {
56 readonly operation: string;
57 readonly provider: SourceControlProviderKind;
58 readonly detail: string;
59 readonly cause?: unknown;
60}): SourceControlRepositoryError {
61 return new SourceControlRepositoryError({
62 provider: input.provider,
63 operation: input.operation,
64 detail: input.detail,
65 ...(input.cause === undefined ? {} : { cause: input.cause }),
66 });
67}
68
6942function mapRepositoryError(operation: string, provider: SourceControlProviderKind) {
7043 return Effect.mapError((cause: unknown) =>
7144 isSourceControlRepositoryError(cause)
7245 ? cause
73 : repositoryError({
46 : new SourceControlRepositoryError({
7447 operation,
7548 provider,
76 detail: detailFromUnknown(cause),
49 detail: "The source control operation could not be completed.",
7750 cause,
7851 }),
7952 );
......@@ -130,7 +103,7 @@ export const make = Effect.gen(function* () {
130103 }
131104
132105 return Effect.fail(
133 repositoryError({
106 new SourceControlRepositoryError({
134107 operation: input.operation,
135108 provider: input.provider,
136109 detail: "Choose a source control provider before continuing.",
......@@ -157,7 +130,7 @@ export const make = Effect.gen(function* () {
157130 function* (destinationPath: string) {
158131 const trimmed = destinationPath.trim();
159132 if (trimmed.length === 0) {
160 return yield* repositoryError({
133 return yield* new SourceControlRepositoryError({
161134 operation: "cloneRepository",
162135 provider: "unknown",
163136 detail: "Choose a destination path before cloning.",
......@@ -171,21 +144,22 @@ export const make = Effect.gen(function* () {
171144 const prepareDestination = Effect.fn("SourceControlRepositoryService.prepareDestination")(
172145 function* (destinationPath: string) {
173146 const normalizedDestination = yield* normalizeDestinationPath(destinationPath);
174 if (yield* fileSystem.exists(normalizedDestination).pipe(Effect.orElseSucceed(() => false))) {
147 if (yield* fileSystem.exists(normalizedDestination)) {
175148 const entries = yield* fileSystem
176149 .readDirectory(normalizedDestination, { recursive: false })
177150 .pipe(
178 Effect.mapError((cause) =>
179 repositoryError({
180 operation: "cloneRepository",
181 provider: "unknown",
182 detail: "Destination path already exists and is not a directory.",
183 cause,
184 }),
151 Effect.mapError(
152 (cause) =>
153 new SourceControlRepositoryError({
154 operation: "cloneRepository",
155 provider: "unknown",
156 detail: "Destination path already exists and is not a directory.",
157 cause,
158 }),
185159 ),
186160 );
187161 if (entries.length > 0) {
188 return yield* repositoryError({
162 return yield* new SourceControlRepositoryError({
189163 operation: "cloneRepository",
190164 provider: "unknown",
191165 detail: "Destination path already exists and is not empty.",
......@@ -222,7 +196,7 @@ export const make = Effect.gen(function* () {
222196 }
223197
224198 if (!remoteUrl) {
225 return yield* repositoryError({
199 return yield* new SourceControlRepositoryError({
226200 operation: "cloneRepository",
227201 provider,
228202 detail: "Enter a repository path or clone URL before cloning.",