From 43c96d55db17591b4e1599ad2c89c684cf0e52b7 Mon Sep 17 00:00:00 2001 From: Andras Hollo Date: Thu, 28 May 2026 22:16:24 -0700 Subject: [PATCH] fix: cache failed build output so gradle-get-details works for failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gradle.build() throws BUILD_FAILED before handleGradleBuildTool generates a buildId or caches output, so a failed build leaves nothing for gradle-get-details to fetch — the one build you most need to inspect is the only one you can't, and the tool's own "Check gradle-get-details" suggestion is a dead end for failures. Carry the full output on the thrown error's context, then in the tool handler catch BUILD_FAILED, cache {fullOutput, result, operation} under a fresh buildId (same shape as the success path), and rethrow the same BUILD_FAILED error with that buildId in context (and without the bulky fullOutput inlined). gradle-get-details { id, detailType: "errors" } now returns the compiler errors for failed builds exactly as for successful ones, preserving the token-optimized contract instead of dumping raw logs. Adds a round-trip test (failure -> cache -> get-details errors). Co-Authored-By: Claude Opus 4.8 --- src/adapters/gradle.ts | 4 +- src/tools/gradle-build.ts | 41 +++++++++++--- src/types/errors.ts | 2 + tests/tools/gradle-build-failure.test.ts | 68 ++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 tests/tools/gradle-build-failure.test.ts diff --git a/src/adapters/gradle.ts b/src/adapters/gradle.ts index 0c0a0aa..01c884d 100644 --- a/src/adapters/gradle.ts +++ b/src/adapters/gradle.ts @@ -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 } ); } diff --git a/src/tools/gradle-build.ts b/src/tools/gradle-build.ts index 13c8d2b..a82f617 100644 --- a/src/tools/gradle-build.ts +++ b/src/tools/gradle-build.ts @@ -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"; @@ -16,12 +16,41 @@ export async function handleGradleBuildTool( input: GradleBuildInput, context: ServerContext ): Promise> { - 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, diff --git a/src/types/errors.ts b/src/types/errors.ts index 38603a5..bdc5db6 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -53,6 +53,8 @@ export interface ErrorContext { stderr?: string; checkedPaths?: string[]; buildResult?: Record; + fullOutput?: string; + buildId?: string; } export interface ToolError { diff --git a/tests/tools/gradle-build-failure.test.ts b/tests/tools/gradle-build-failure.test.ts new file mode 100644 index 0000000..d5291eb --- /dev/null +++ b/tests/tools/gradle-build-failure.test.ts @@ -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"); + }); +});