Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/adapters/gradle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export class GradleAdapter {
ErrorCode.BUILD_FAILED,
`Build failed: ${parsed.failedTask || "unknown error"}`,
"Check gradle-get-details for full error output",
{ buildResult: { ...parsed } }
// Carry the full build output so the tool handler can cache it under a
// buildId, making gradle-get-details work for failed builds too.
{ buildResult: { ...parsed }, fullOutput: result.stdout + result.stderr }
);
}

Expand Down
41 changes: 35 additions & 6 deletions src/tools/gradle-build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import { ServerContext } from "../server.js";
import { CACHE_TTLS } from "../types/index.js";
import { CACHE_TTLS, ReplicantError, ErrorCode } from "../types/index.js";
import { toolSchema } from "../schemas/inputs.js";
import { toMcpJsonSchema } from "../schemas/derive.js";

Expand All @@ -16,12 +16,41 @@ export async function handleGradleBuildTool(
input: GradleBuildInput,
context: ServerContext
): Promise<Record<string, unknown>> {
const { result, fullOutput } = await context.gradle.build(
input.operation,
input.module,
input.flavor
);
let buildResult;
try {
buildResult = await context.gradle.build(
input.operation,
input.module,
input.flavor
);
} catch (error) {
// A failed build still has useful output (compiler errors). Cache it under
// a buildId and rethrow with that id so gradle-get-details can fetch the
// full errors — same retrieval path as a successful build.
if (error instanceof ReplicantError && error.code === ErrorCode.BUILD_FAILED) {
const failureContext = error.context ?? {};
const buildId = context.cache.generateId("build");
context.cache.set(
buildId,
{
fullOutput: failureContext.fullOutput ?? "",
result: failureContext.buildResult ?? {},
operation: input.operation,
},
"build",
CACHE_TTLS.BUILD_OUTPUT
);
throw new ReplicantError(
ErrorCode.BUILD_FAILED,
error.message,
`Fetch full error output: gradle-get-details { id: "${buildId}", detailType: "errors" }`,
{ buildResult: failureContext.buildResult, buildId }
);
}
throw error;
}

const { result, fullOutput } = buildResult;
const buildId = context.cache.generateId("build");
context.cache.set(
buildId,
Expand Down
2 changes: 2 additions & 0 deletions src/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export interface ErrorContext {
stderr?: string;
checkedPaths?: string[];
buildResult?: Record<string, unknown>;
fullOutput?: string;
buildId?: string;
}

export interface ToolError {
Expand Down
68 changes: 68 additions & 0 deletions tests/tools/gradle-build-failure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, it, expect, vi } from "vitest";
import { handleGradleBuildTool } from "../../src/tools/gradle-build.js";
import { handleGradleGetDetailsTool } from "../../src/tools/gradle-get-details.js";
import { CacheManager } from "../../src/services/index.js";
import { ReplicantError, ErrorCode } from "../../src/types/errors.js";
import { ServerContext } from "../../src/server.js";

const FAILED_OUTPUT = [
"> Task :app:compileDjborzeDebugKotlin FAILED",
"e: file:///app/src/main/java/Foo.kt:12:34 Unresolved reference: bar",
"BUILD FAILED in 2s",
].join("\n");

function contextWithFailingBuild(cache: CacheManager): ServerContext {
return {
gradle: {
build: vi.fn().mockRejectedValue(
new ReplicantError(
ErrorCode.BUILD_FAILED,
"Build failed: :app:compileDjborzeDebugKotlin",
"Check gradle-get-details for full error output",
{
buildResult: { success: false, failedTask: ":app:compileDjborzeDebugKotlin", errors: 1 },
fullOutput: FAILED_OUTPUT,
}
)
),
},
cache,
} as unknown as ServerContext;
}

describe("gradle-build failure caching", () => {
it("caches a failed build under a buildId and rethrows BUILD_FAILED with that id", async () => {
const cache = new CacheManager();
const context = contextWithFailingBuild(cache);

const error = await handleGradleBuildTool(
{ operation: "assembleDebug", flavor: "djborze" },
context
).catch((e) => e);

expect(error).toBeInstanceOf(ReplicantError);
expect(error.code).toBe(ErrorCode.BUILD_FAILED);
expect(error.context?.buildId).toBeDefined();
// The bulky output must NOT be inlined in the rethrown error.
expect(error.context?.fullOutput).toBeUndefined();
});

it("makes the failed build's errors retrievable via gradle-get-details", async () => {
const cache = new CacheManager();
const context = contextWithFailingBuild(cache);

const error = await handleGradleBuildTool(
{ operation: "assembleDebug", flavor: "djborze" },
context
).catch((e) => e);

const buildId = error.context.buildId as string;
const details = await handleGradleGetDetailsTool(
{ id: buildId, detailType: "errors" },
context
);

expect(details.detailType).toBe("errors");
expect(String(details.errors)).toContain("Unresolved reference: bar");
});
});
Loading