From 7cdb7981c3612bc42bd0153fddd9c33b639918ed Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:13:23 -0700 Subject: [PATCH] fix(apktool): name host requirements in configuration refusals (#1946) A missing apktool launcher projected the generic "unavailable for the current target" message and remediation, leaving the actual reason only in details.reason. A relative REA_APKTOOL_COMMAND escaped as UNKNOWN because command selection ran before the try that maps configuration failures. Resolve the selection inside that try for both execution and availability, and pass the refusal reason as userMessage, as JADX and the adapters fixed in #1224 do. Co-authored-by: Claude Opus 5.5 --- src/apktool/ApktoolProvider.ts | 24 ++++++++++------- tests/boundary/cli/apktoolCli.test.ts | 38 ++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/apktool/ApktoolProvider.ts b/src/apktool/ApktoolProvider.ts index c9a9c2804..eba734883 100644 --- a/src/apktool/ApktoolProvider.ts +++ b/src/apktool/ApktoolProvider.ts @@ -153,12 +153,13 @@ const runApktool = async ( invocation = apktoolInvocation(selection, arguments_, hostEnvironment); } catch (cause) { if (!(cause instanceof WindowsBatchCommandError)) throw cause; + const reason = `The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`; return err( new AnalysisCapabilityUnavailableError( APKTOOL_PROVIDER_IDENTITY.id, operation, - `The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`, - { cause }, + reason, + { cause, userMessage: reason }, ), ); } @@ -345,8 +346,8 @@ export class ApktoolProvider { async #inspectAvailability( signal: AbortSignal, ): Promise { - const selection = this.selection(); try { + const selection = this.selection(); await inspectApktoolClient(selection, signal, this.environment); throwIfCancelled("inspect_apktool_client", signal); return { @@ -397,9 +398,10 @@ export class ApktoolProvider { ): Promise> { if (signal?.aborted === true) return err(new AnalysisCancelledError(request.operation)); - const selection = this.selection(); + let selection: ApktoolCommandSelection; let identity: ApktoolClientIdentity; try { + selection = this.selection(); identity = await inspectApktoolClient( selection, signal, @@ -407,17 +409,21 @@ export class ApktoolProvider { ); } catch (cause) { if (cause instanceof AnalysisCancelledError) return err(cause); - if (cause instanceof ApktoolConfigurationFailure) + if (cause instanceof ApktoolConfigurationFailure) { + // A host configuration refusal; the target is not the problem. + const reason = + typeof cause.diagnostics.remediation === "string" + ? `${cause.message}. ${cause.diagnostics.remediation}.` + : `${cause.message}.`; return err( new AnalysisCapabilityUnavailableError( APKTOOL_PROVIDER_IDENTITY.id, request.operation, - typeof cause.diagnostics.remediation === "string" - ? `${cause.message} ${cause.diagnostics.remediation}` - : cause.message, - { cause }, + reason, + { cause, userMessage: reason }, ), ); + } return err( new AnalysisProtocolError( `apktool version probe failed for ${request.operation}`, diff --git a/tests/boundary/cli/apktoolCli.test.ts b/tests/boundary/cli/apktoolCli.test.ts index 65752906f..e3415e6f0 100644 --- a/tests/boundary/cli/apktoolCli.test.ts +++ b/tests/boundary/cli/apktoolCli.test.ts @@ -12,13 +12,37 @@ it.each(["inspect-apktool-client", "decode-android-resources"])( }, ); -it("reports a missing apktool launcher with recovery guidance", async () => { - const result = await serve(["inspect-apktool-client", "--json"], { - REA_APKTOOL_COMMAND: "/nonexistent/apktool", - }); - expect(result.stdout).toContain("capability_unavailable"); - expect(result.stdout).toContain("REA_APKTOOL_COMMAND"); -}); +it.each([ + [ + ["inspect-apktool-client", "--json"], + "/nonexistent/apktool", + "The selected apktool launcher was not found: /nonexistent/apktool. Set REA_APKTOOL_COMMAND", + ], + [ + ["decode-android-resources", "/targets/app.apk", "--json"], + "/nonexistent/apktool", + "The selected apktool launcher was not found: /nonexistent/apktool. Set REA_APKTOOL_COMMAND", + ], + [ + ["decode-android-resources", "/targets/app.apk", "--json"], + "relative/apktool", + "REA_APKTOOL_COMMAND must be an absolute path to the apktool launcher.", + ], +])( + "%j with REA_APKTOOL_COMMAND=%s names the host requirement", + async (argv, command, reason) => { + const result = await serve(argv, { REA_APKTOOL_COMMAND: command }); + const output = JSON.parse(result.stdout) as { + code?: unknown; + message?: unknown; + remediation?: { action?: unknown }; + }; + expect(output.code).toBe("capability_unavailable"); + // The target is not the problem; the refusal must say what to configure. + expect(output.message).toEqual(expect.stringContaining(reason)); + expect(output.remediation?.action).toEqual(expect.stringContaining(reason)); + }, +); it("validates the locale option before launching apktool", async () => { const result = await serve([