diff --git a/src/apktool/ApktoolProvider.ts b/src/apktool/ApktoolProvider.ts index c9a9c28040..eba734883b 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 65752906fa..e3415e6f0b 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([