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";...@@ -6,8 +6,14 @@ import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";8import * as Option from "effect/Option";
9import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";9import {
1010 HttpClient,
11 HttpClientError,
12 HttpClientRequest,
13 HttpClientResponse,
14} from "effect/unstable/http";
15
16import { GitCommandError } from "@t3tools/contracts";
11import * as BitbucketApi from "./BitbucketApi.ts";17import * as BitbucketApi from "./BitbucketApi.ts";
12import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";18import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
13import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";19import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
...@@ -53,10 +59,15 @@ const repositoryJson = {...@@ -53,10 +59,15 @@ const repositoryJson = {
5359
54function makeLayer(input: {60function makeLayer(input: {
55 readonly response: (request: HttpClientRequest.HttpClientRequest) => Response;61 readonly response: (request: HttpClientRequest.HttpClientRequest) => Response;
62 readonly requestFailure?: (
63 request: HttpClientRequest.HttpClientRequest,
64 ) => HttpClientError.HttpClientError;
56 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;65 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
57}) {66}) {
58 const execute = vi.fn((request: HttpClientRequest.HttpClientRequest) =>67 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))),
60 );71 );
61 const gitMock = {72 const gitMock = {
62 readConfigValue: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["readConfigValue"]>(() =>73 readConfigValue: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["readConfigValue"]>(() =>
...@@ -497,6 +508,42 @@ it.effect("reports auth status through the Bitbucket REST /user endpoint", () =>...@@ -497,6 +508,42 @@ it.effect("reports auth status through the Bitbucket REST /user endpoint", () =>
497 }).pipe(Effect.provide(layer));508 }).pipe(Effect.provide(layer));
498});509});
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
500it.effect("checks out same-repository pull requests with the existing Bitbucket remote", () => {547it.effect("checks out same-repository pull requests with the existing Bitbucket remote", () => {
501 const { git, layer } = makeLayer({548 const { git, layer } = makeLayer({
502 response: () =>549 response: () =>
...@@ -549,6 +596,50 @@ it.effect("checks out same-repository pull requests with the existing Bitbucket...@@ -549,6 +596,50 @@ it.effect("checks out same-repository pull requests with the existing Bitbucket
549 }).pipe(Effect.provide(layer));596 }).pipe(Effect.provide(layer));
550});597});
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
552it.effect("checks out fork pull requests through an ensured fork remote", () => {643it.effect("checks out fork pull requests through an ensured fork remote", () => {
553 const { git, layer } = makeLayer({644 const { git, layer } = makeLayer({
554 response: (request) => {645 response: (request) => {
apps/server/src/sourceControl/BitbucketApi.ts+9-10
...@@ -338,14 +338,6 @@ function authFromConfig(...@@ -338,14 +338,6 @@ function authFromConfig(
338 };338 };
339}339}
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
349function responseError(341function responseError(
350 operation: string,342 operation: string,
351 response: HttpClientResponse.HttpClientResponse,343 response: HttpClientResponse.HttpClientResponse,
...@@ -412,7 +404,14 @@ export const make = Effect.gen(function* () {...@@ -412,7 +404,14 @@ export const make = Effect.gen(function* () {
412 schema: S,404 schema: S,
413 ): Effect.Effect<S["Type"], BitbucketApiError, S["DecodingServices"]> =>405 ): Effect.Effect<S["Type"], BitbucketApiError, S["DecodingServices"]> =>
414 httpClient.execute(withAuth(request.pipe(HttpClientRequest.acceptJson))).pipe(406 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 ),
416 Effect.flatMap((response) => decodeResponse(operation, schema, response)),415 Effect.flatMap((response) => decodeResponse(operation, schema, response)),
417 );416 );
418417
...@@ -746,7 +745,7 @@ export const make = Effect.gen(function* () {...@@ -746,7 +745,7 @@ export const make = Effect.gen(function* () {
746 ? cause745 ? cause
747 : new BitbucketApiError({746 : new BitbucketApiError({
748 operation: "checkoutPullRequest",747 operation: "checkoutPullRequest",
749 detail: cause instanceof Error ? cause.message : String(cause),748 detail: "Failed to check out the Bitbucket pull request.",
750 cause,749 cause,
751 }),750 }),
752 ),751 ),