authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:13:02-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:13:02-07:00
log3ecf3685ba7dbaca04f6a1219a8d42e6094dc526
tree3c7964e417ccd71e4766698a633e05115c55b859
parent716ae73c40e988b462ab73f2bd7aaca789054e35
signature Signed by PGP key B5690EEEBB952194

[codex] structure Bitbucket API failures (#3332)

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

2 files changed, 103 insertions(+), 13 deletions(-)

apps/server/src/sourceControl/BitbucketApi.test.ts+94-3
......@@ -6,8 +6,14 @@ import * as Effect from "effect/Effect";
66import * as FileSystem from "effect/FileSystem";
77import * as Layer from "effect/Layer";
88import * as Option from "effect/Option";
9import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
10
9import {
10 HttpClient,
11 HttpClientError,
12 HttpClientRequest,
13 HttpClientResponse,
14} from "effect/unstable/http";
15
16import { GitCommandError } from "@t3tools/contracts";
1117import * as BitbucketApi from "./BitbucketApi.ts";
1218import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
1319import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
......@@ -53,10 +59,15 @@ const repositoryJson = {
5359
5460function makeLayer(input: {
5561 readonly response: (request: HttpClientRequest.HttpClientRequest) => Response;
62 readonly requestFailure?: (
63 request: HttpClientRequest.HttpClientRequest,
64 ) => HttpClientError.HttpClientError;
5665 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
5766}) {
5867 const execute = vi.fn((request: HttpClientRequest.HttpClientRequest) =>
59 Effect.succeed(HttpClientResponse.fromWeb(request, input.response(request))),
68 input.requestFailure
69 ? Effect.fail(input.requestFailure(request))
70 : Effect.succeed(HttpClientResponse.fromWeb(request, input.response(request))),
6071 );
6172 const gitMock = {
6273 readConfigValue: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["readConfigValue"]>(() =>
......@@ -497,6 +508,42 @@ it.effect("reports auth status through the Bitbucket REST /user endpoint", () =>
497508 }).pipe(Effect.provide(layer));
498509});
499510
511it.effect("preserves the HTTP client failure without deriving the domain message from it", () => {
512 const transportCause = new Error("socket reset by peer");
513 let requestFailure: HttpClientError.HttpClientError | undefined;
514 const { layer } = makeLayer({
515 response: () => Response.json({}),
516 requestFailure: (request) => {
517 requestFailure = new HttpClientError.HttpClientError({
518 reason: new HttpClientError.TransportError({
519 request,
520 cause: transportCause,
521 }),
522 });
523 return requestFailure;
524 },
525 });
526
527 return Effect.gen(function* () {
528 const bitbucket = yield* BitbucketApi.BitbucketApi;
529 const error = yield* Effect.flip(
530 bitbucket.getPullRequest({
531 cwd: "/repo",
532 reference: "42",
533 }),
534 );
535
536 assert.strictEqual(error.operation, "getPullRequest");
537 assert.strictEqual(error.detail, "Failed to send the Bitbucket request.");
538 assert.strictEqual(
539 error.message,
540 "Bitbucket API failed in getPullRequest: Failed to send the Bitbucket request.",
541 );
542 assert.strictEqual(error.cause, requestFailure);
543 assert.strictEqual(requestFailure?.cause, transportCause);
544 }).pipe(Effect.provide(layer));
545});
546
500547it.effect("checks out same-repository pull requests with the existing Bitbucket remote", () => {
501548 const { git, layer } = makeLayer({
502549 response: () =>
......@@ -549,6 +596,50 @@ it.effect("checks out same-repository pull requests with the existing Bitbucket
549596 }).pipe(Effect.provide(layer));
550597});
551598
599it.effect("preserves Git checkout failures without deriving the domain message from them", () => {
600 const gitCause = new GitCommandError({
601 operation: "fetchRemoteBranch",
602 command: "git fetch origin feature/source-control",
603 cwd: "/repo",
604 detail: "remote rejected the request",
605 });
606 const { layer } = makeLayer({
607 response: () =>
608 Response.json({
609 ...bitbucketPullRequest,
610 source: {
611 branch: { name: "feature/source-control" },
612 repository: {
613 full_name: "pingdotgg/t3code",
614 workspace: { slug: "pingdotgg" },
615 },
616 },
617 }),
618 git: {
619 fetchRemoteBranch: () => Effect.fail(gitCause),
620 },
621 });
622
623 return Effect.gen(function* () {
624 const bitbucket = yield* BitbucketApi.BitbucketApi;
625 const error = yield* Effect.flip(
626 bitbucket.checkoutPullRequest({
627 cwd: "/repo",
628 reference: "42",
629 force: true,
630 }),
631 );
632
633 assert.strictEqual(error.operation, "checkoutPullRequest");
634 assert.strictEqual(error.detail, "Failed to check out the Bitbucket pull request.");
635 assert.strictEqual(
636 error.message,
637 "Bitbucket API failed in checkoutPullRequest: Failed to check out the Bitbucket pull request.",
638 );
639 assert.strictEqual(error.cause, gitCause);
640 }).pipe(Effect.provide(layer));
641});
642
552643it.effect("checks out fork pull requests through an ensured fork remote", () => {
553644 const { git, layer } = makeLayer({
554645 response: (request) => {
apps/server/src/sourceControl/BitbucketApi.ts+9-10
......@@ -338,14 +338,6 @@ function authFromConfig(
338338 };
339339}
340340
341function requestError(operation: string, cause: unknown): BitbucketApiError {
342 return new BitbucketApiError({
343 operation,
344 detail: cause instanceof Error ? cause.message : String(cause),
345 cause,
346 });
347}
348
349341function responseError(
350342 operation: string,
351343 response: HttpClientResponse.HttpClientResponse,
......@@ -412,7 +404,14 @@ export const make = Effect.gen(function* () {
412404 schema: S,
413405 ): Effect.Effect<S["Type"], BitbucketApiError, S["DecodingServices"]> =>
414406 httpClient.execute(withAuth(request.pipe(HttpClientRequest.acceptJson))).pipe(
415 Effect.mapError((cause) => requestError(operation, cause)),
407 Effect.mapError(
408 (cause) =>
409 new BitbucketApiError({
410 operation,
411 detail: "Failed to send the Bitbucket request.",
412 cause,
413 }),
414 ),
416415 Effect.flatMap((response) => decodeResponse(operation, schema, response)),
417416 );
418417
......@@ -746,7 +745,7 @@ export const make = Effect.gen(function* () {
746745 ? cause
747746 : new BitbucketApiError({
748747 operation: "checkoutPullRequest",
749 detail: cause instanceof Error ? cause.message : String(cause),
748 detail: "Failed to check out the Bitbucket pull request.",
750749 cause,
751750 }),
752751 ),