Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92fddf3176 | ||
|
|
9655845bbc |
@@ -10,12 +10,12 @@ import type {
|
|||||||
} from "../application/AnalysisProvider.js";
|
} from "../application/AnalysisProvider.js";
|
||||||
import {
|
import {
|
||||||
createAnalysisExecution,
|
createAnalysisExecution,
|
||||||
|
providerConfigurationRefusal,
|
||||||
type AnalysisExecution,
|
type AnalysisExecution,
|
||||||
} from "../application/AnalysisProvider.js";
|
} from "../application/AnalysisProvider.js";
|
||||||
import type { AdbRequest } from "../domain/adb/adbDeviceAnalysis.js";
|
import type { AdbRequest } from "../domain/adb/adbDeviceAnalysis.js";
|
||||||
import {
|
import {
|
||||||
AnalysisArtifactChangedError,
|
AnalysisArtifactChangedError,
|
||||||
AnalysisCapabilityUnavailableError,
|
|
||||||
AnalysisCancelledError,
|
AnalysisCancelledError,
|
||||||
AnalysisInputError,
|
AnalysisInputError,
|
||||||
AnalysisOutputError,
|
AnalysisOutputError,
|
||||||
@@ -417,8 +417,8 @@ export class AdbProvider {
|
|||||||
async inspectAvailability(
|
async inspectAvailability(
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<ProviderAvailability> {
|
): Promise<ProviderAvailability> {
|
||||||
const selection = this.selection();
|
|
||||||
try {
|
try {
|
||||||
|
const selection = this.selection();
|
||||||
await inspectAdbClient(selection, signal);
|
await inspectAdbClient(selection, signal);
|
||||||
return {
|
return {
|
||||||
status: "available",
|
status: "available",
|
||||||
@@ -555,21 +555,19 @@ export class AdbProvider {
|
|||||||
const signal = options?.signal;
|
const signal = options?.signal;
|
||||||
if (signal?.aborted === true)
|
if (signal?.aborted === true)
|
||||||
return err(new AnalysisCancelledError(request.operation));
|
return err(new AnalysisCancelledError(request.operation));
|
||||||
const selection = this.selection();
|
let selection: AdbBinarySelection;
|
||||||
let identity: AdbClientIdentity;
|
let identity: AdbClientIdentity;
|
||||||
try {
|
try {
|
||||||
|
selection = this.selection();
|
||||||
identity = await inspectAdbClient(selection, signal);
|
identity = await inspectAdbClient(selection, signal);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (cause instanceof AnalysisCancelledError) return err(cause);
|
if (cause instanceof AnalysisCancelledError) return err(cause);
|
||||||
if (cause instanceof AdbConfigurationFailure)
|
if (cause instanceof AdbConfigurationFailure)
|
||||||
return err(
|
return err(
|
||||||
new AnalysisCapabilityUnavailableError(
|
providerConfigurationRefusal(
|
||||||
ADB_PROVIDER_IDENTITY.id,
|
ADB_PROVIDER_IDENTITY.id,
|
||||||
request.operation,
|
request.operation,
|
||||||
typeof cause.diagnostics.remediation === "string"
|
cause,
|
||||||
? `${cause.message} ${cause.diagnostics.remediation}`
|
|
||||||
: cause.message,
|
|
||||||
{ cause },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return err(
|
return err(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
} from "../application/AnalysisProvider.js";
|
} from "../application/AnalysisProvider.js";
|
||||||
import {
|
import {
|
||||||
createAnalysisExecution,
|
createAnalysisExecution,
|
||||||
|
providerConfigurationRefusal,
|
||||||
type AnalysisExecution,
|
type AnalysisExecution,
|
||||||
} from "../application/AnalysisProvider.js";
|
} from "../application/AnalysisProvider.js";
|
||||||
import type { ApktoolRequest } from "../domain/apktool/apktoolResourceAnalysis.js";
|
import type { ApktoolRequest } from "../domain/apktool/apktoolResourceAnalysis.js";
|
||||||
@@ -153,12 +154,13 @@ const runApktool = async (
|
|||||||
invocation = apktoolInvocation(selection, arguments_, hostEnvironment);
|
invocation = apktoolInvocation(selection, arguments_, hostEnvironment);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (!(cause instanceof WindowsBatchCommandError)) throw 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(
|
return err(
|
||||||
new AnalysisCapabilityUnavailableError(
|
new AnalysisCapabilityUnavailableError(
|
||||||
APKTOOL_PROVIDER_IDENTITY.id,
|
APKTOOL_PROVIDER_IDENTITY.id,
|
||||||
operation,
|
operation,
|
||||||
`The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`,
|
reason,
|
||||||
{ cause },
|
{ cause, userMessage: reason },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -345,8 +347,8 @@ export class ApktoolProvider {
|
|||||||
async #inspectAvailability(
|
async #inspectAvailability(
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
): Promise<ProviderAvailability> {
|
): Promise<ProviderAvailability> {
|
||||||
const selection = this.selection();
|
|
||||||
try {
|
try {
|
||||||
|
const selection = this.selection();
|
||||||
await inspectApktoolClient(selection, signal, this.environment);
|
await inspectApktoolClient(selection, signal, this.environment);
|
||||||
throwIfCancelled("inspect_apktool_client", signal);
|
throwIfCancelled("inspect_apktool_client", signal);
|
||||||
return {
|
return {
|
||||||
@@ -397,9 +399,10 @@ export class ApktoolProvider {
|
|||||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||||
if (signal?.aborted === true)
|
if (signal?.aborted === true)
|
||||||
return err(new AnalysisCancelledError(request.operation));
|
return err(new AnalysisCancelledError(request.operation));
|
||||||
const selection = this.selection();
|
let selection: ApktoolCommandSelection;
|
||||||
let identity: ApktoolClientIdentity;
|
let identity: ApktoolClientIdentity;
|
||||||
try {
|
try {
|
||||||
|
selection = this.selection();
|
||||||
identity = await inspectApktoolClient(
|
identity = await inspectApktoolClient(
|
||||||
selection,
|
selection,
|
||||||
signal,
|
signal,
|
||||||
@@ -409,13 +412,10 @@ export class ApktoolProvider {
|
|||||||
if (cause instanceof AnalysisCancelledError) return err(cause);
|
if (cause instanceof AnalysisCancelledError) return err(cause);
|
||||||
if (cause instanceof ApktoolConfigurationFailure)
|
if (cause instanceof ApktoolConfigurationFailure)
|
||||||
return err(
|
return err(
|
||||||
new AnalysisCapabilityUnavailableError(
|
providerConfigurationRefusal(
|
||||||
APKTOOL_PROVIDER_IDENTITY.id,
|
APKTOOL_PROVIDER_IDENTITY.id,
|
||||||
request.operation,
|
request.operation,
|
||||||
typeof cause.diagnostics.remediation === "string"
|
cause,
|
||||||
? `${cause.message} ${cause.diagnostics.remediation}`
|
|
||||||
: cause.message,
|
|
||||||
{ cause },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return err(
|
return err(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { NativeToolName } from "../contracts/native/nativeToolContracts.js"
|
|||||||
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
|
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
|
||||||
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
|
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
|
||||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||||
|
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||||
import type { JsonValue } from "../domain/jsonValue.js";
|
import type { JsonValue } from "../domain/jsonValue.js";
|
||||||
import type { Result } from "../domain/result.js";
|
import type { Result } from "../domain/result.js";
|
||||||
@@ -210,6 +211,31 @@ export type ProviderAvailability =
|
|||||||
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** A host tool configuration failure observed before any target work. */
|
||||||
|
export interface ProviderConfigurationFailure extends Error {
|
||||||
|
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refuse an operation because the host tool is misconfigured. The reason,
|
||||||
|
* not the generic target advice, tells the caller what to change.
|
||||||
|
*/
|
||||||
|
export const providerConfigurationRefusal = (
|
||||||
|
providerId: string,
|
||||||
|
operation: string,
|
||||||
|
failure: ProviderConfigurationFailure,
|
||||||
|
): AnalysisCapabilityUnavailableError => {
|
||||||
|
const remediation = failure.diagnostics.remediation;
|
||||||
|
const reason =
|
||||||
|
typeof remediation === "string"
|
||||||
|
? `${failure.message}. ${remediation}.`
|
||||||
|
: `${failure.message}.`;
|
||||||
|
return new AnalysisCapabilityUnavailableError(providerId, operation, reason, {
|
||||||
|
cause: failure,
|
||||||
|
userMessage: reason,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/** Provider-owned target support observation before profile resolution. */
|
/** Provider-owned target support observation before profile resolution. */
|
||||||
export type ProviderTargetSupport =
|
export type ProviderTargetSupport =
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { expect, it } from "vitest";
|
import { expect, it } from "vitest";
|
||||||
|
|
||||||
import { serveCli as serve } from "./cliServeHarness.js";
|
import {
|
||||||
|
expectConfigurationRefusal,
|
||||||
|
serveCli as serve,
|
||||||
|
} from "./cliServeHarness.js";
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
["inspect-adb-client"],
|
["inspect-adb-client"],
|
||||||
@@ -34,13 +37,29 @@ it.each([
|
|||||||
expect(schema).toBeTruthy();
|
expect(schema).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reports a missing adb binary with recovery guidance", async () => {
|
it.each([
|
||||||
const result = await serve(["inspect-adb-client", "--json"], {
|
[
|
||||||
REA_ADB_PATH: "/nonexistent/adb",
|
["inspect-adb-client", "--json"],
|
||||||
});
|
"/nonexistent/adb",
|
||||||
expect(result.stdout).toContain("capability_unavailable");
|
"The selected adb binary was not found: /nonexistent/adb. Set REA_ADB_PATH",
|
||||||
expect(result.stdout).toContain("REA_ADB_PATH");
|
],
|
||||||
});
|
[
|
||||||
|
["list-adb-devices", "--json"],
|
||||||
|
"/nonexistent/adb",
|
||||||
|
"The selected adb binary was not found: /nonexistent/adb. Set REA_ADB_PATH",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["list-adb-devices", "--json"],
|
||||||
|
"relative/adb",
|
||||||
|
"REA_ADB_PATH must be an absolute path to the adb binary.",
|
||||||
|
],
|
||||||
|
])(
|
||||||
|
"%j with REA_ADB_PATH=%s names the host requirement",
|
||||||
|
async (argv, binary, reason) => {
|
||||||
|
const result = await serve(argv, { REA_ADB_PATH: binary });
|
||||||
|
expectConfigurationRefusal(result, reason);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("validates device serials before contacting any device", async () => {
|
it("validates device serials before contacting any device", async () => {
|
||||||
const result = await serve(["inspect-adb-device", "bad serial", "--json"]);
|
const result = await serve(["inspect-adb-device", "bad serial", "--json"]);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { expect, it } from "vitest";
|
import { expect, it } from "vitest";
|
||||||
|
|
||||||
import { serveCli as serve } from "./cliServeHarness.js";
|
import {
|
||||||
|
expectConfigurationRefusal,
|
||||||
|
serveCli as serve,
|
||||||
|
} from "./cliServeHarness.js";
|
||||||
|
|
||||||
it.each(["inspect-apktool-client", "decode-android-resources"])(
|
it.each(["inspect-apktool-client", "decode-android-resources"])(
|
||||||
"%s advertises a schema through the public CLI",
|
"%s advertises a schema through the public CLI",
|
||||||
@@ -12,13 +15,29 @@ it.each(["inspect-apktool-client", "decode-android-resources"])(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("reports a missing apktool launcher with recovery guidance", async () => {
|
it.each([
|
||||||
const result = await serve(["inspect-apktool-client", "--json"], {
|
[
|
||||||
REA_APKTOOL_COMMAND: "/nonexistent/apktool",
|
["inspect-apktool-client", "--json"],
|
||||||
});
|
"/nonexistent/apktool",
|
||||||
expect(result.stdout).toContain("capability_unavailable");
|
"The selected apktool launcher was not found: /nonexistent/apktool. Set REA_APKTOOL_COMMAND",
|
||||||
expect(result.stdout).toContain("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 });
|
||||||
|
expectConfigurationRefusal(result, reason);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("validates the locale option before launching apktool", async () => {
|
it("validates the locale option before launching apktool", async () => {
|
||||||
const result = await serve([
|
const result = await serve([
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { expect } from "vitest";
|
||||||
|
|
||||||
import { createCli } from "../../../src/cli.js";
|
import { createCli } from "../../../src/cli.js";
|
||||||
|
|
||||||
/** Captured CLI invocation for boundary tests. */
|
/** Captured CLI invocation for boundary tests. */
|
||||||
@@ -25,3 +27,18 @@ export const serveCli = async (
|
|||||||
});
|
});
|
||||||
return { stdout, exitCode };
|
return { stdout, exitCode };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** A host tool configuration refusal names what to configure, not the target. */
|
||||||
|
export const expectConfigurationRefusal = (
|
||||||
|
result: ServedCli,
|
||||||
|
reason: string,
|
||||||
|
): void => {
|
||||||
|
const output = JSON.parse(result.stdout) as {
|
||||||
|
code?: unknown;
|
||||||
|
message?: unknown;
|
||||||
|
remediation?: { action?: unknown };
|
||||||
|
};
|
||||||
|
expect(output.code).toBe("capability_unavailable");
|
||||||
|
expect(output.message).toEqual(expect.stringContaining(reason));
|
||||||
|
expect(output.remediation?.action).toEqual(expect.stringContaining(reason));
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user