mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
* fix(android): name the JADX deadline and recover apktool decode JADX operation timeouts now name REA_JADX_OPERATION_TIMEOUT_MS, the effective milliseconds, the 1..2147483647 range, and that a change applies on the next process start. Apktool decode accepts decode_timeout_ms, omitted means 600000, and a timeout names that key. ADB deadlines and apktool and Java version probes stay fixed and say there is no runtime override. Refs #5910 Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(android): derive capture recovery from the fixed allowance Main now stores a fixed allowance instead of a prebuilt remediation string. Apktool and adb capture refusals keep the no-runtime-override sentence in that allowance name, including that decode_timeout_ms does not raise the apktool output cap. Co-authored-by: morluto <morluto@users.noreply.github.com> * test(apktool): observe a real decode deadline and process cleanup A hanging interpreter launcher and its pipe-holding child stay alive until decode_timeout_ms elapses. The provider then reports provider_timeout with completed cleanup, and both processes are gone. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
194 lines
6.4 KiB
TypeScript
194 lines
6.4 KiB
TypeScript
import { ApktoolCommandCleanup } from "./ApktoolCommandCleanup.js";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisTimeoutError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import {
|
|
OwnedCommandFailure,
|
|
runOwnedCommand,
|
|
} from "../process/OwnedCommand.js";
|
|
import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
|
|
import { inspectApktoolClient } from "./ApktoolConfiguration.js";
|
|
import { ApktoolProvider } from "./ApktoolProvider.js";
|
|
|
|
vi.mock("../process/OwnedCommand.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../process/OwnedCommand.js")>()),
|
|
runOwnedCommand: vi.fn(),
|
|
}));
|
|
let commands = new ApktoolCommandCleanup();
|
|
afterEach(async () => {
|
|
await commands.close();
|
|
commands = new ApktoolCommandCleanup();
|
|
vi.mocked(runOwnedCommand).mockReset();
|
|
});
|
|
|
|
const selection = {
|
|
command: "/opt/apktool/apktool",
|
|
commandSource: "environment",
|
|
} as const;
|
|
const owner = {
|
|
stop: vi.fn(async () => ({ status: "verified-cleanup" as const })),
|
|
} as unknown as ProviderProcessSupervisor;
|
|
|
|
it("runs the version probe as an owned interpreter-script process group", async () => {
|
|
vi.mocked(runOwnedCommand).mockImplementationOnce(async (_spawn, limits) => {
|
|
const output = {
|
|
stdout: { text: "2.9.3\n" },
|
|
stderr: { text: "" },
|
|
} as Awaited<ReturnType<typeof runOwnedCommand>>;
|
|
limits.observeCompleted?.(output);
|
|
return output;
|
|
});
|
|
await expect(
|
|
inspectApktoolClient(selection, commands, {
|
|
environment: {},
|
|
platform: "linux",
|
|
}),
|
|
).resolves.toEqual({
|
|
version: "2.9.3",
|
|
supportsDocumentedVersion: true,
|
|
limitations: [],
|
|
});
|
|
expect(vi.mocked(runOwnedCommand).mock.lastCall?.[0]).toMatchObject({
|
|
command: "/opt/apktool/apktool",
|
|
arguments: ["--version"],
|
|
expectedCommand: null,
|
|
});
|
|
expect(vi.mocked(runOwnedCommand).mock.lastCall?.[1]).toMatchObject({
|
|
timeoutMs: 20_000,
|
|
diagnosticBytes: 64 * 1024,
|
|
});
|
|
});
|
|
|
|
it("reports an expired version probe as a typed timeout", async () => {
|
|
vi.mocked(runOwnedCommand).mockRejectedValueOnce(
|
|
new OwnedCommandFailure("timeout", "Command deadline elapsed."),
|
|
);
|
|
const failure = await inspectApktoolClient(selection, commands, {
|
|
environment: {},
|
|
platform: "linux",
|
|
}).catch((cause: unknown) => cause);
|
|
expect(failure).toBeInstanceOf(AnalysisTimeoutError);
|
|
const projected = projectAnalysisError(failure as AnalysisTimeoutError);
|
|
expect(projected).toMatchObject({
|
|
code: "provider_timeout",
|
|
category: "timeout",
|
|
details: { timeout_ms: 20_000 },
|
|
});
|
|
expect(projected.message).toContain("20000 ms");
|
|
expect(projected.message).toContain("no runtime override");
|
|
expect(projected.remediation.action).toContain("decode_timeout_ms");
|
|
expect(projected.remediation.action).toContain("no runtime override");
|
|
});
|
|
|
|
it("retains an unresolved probe cleanup owner and reports the cleanup", async () => {
|
|
vi.mocked(runOwnedCommand).mockRejectedValueOnce(
|
|
new OwnedCommandFailure(
|
|
"timeout",
|
|
"Command deadline elapsed.",
|
|
null,
|
|
"cleanup incomplete",
|
|
undefined,
|
|
["pid:1"],
|
|
owner,
|
|
),
|
|
);
|
|
const failure = await inspectApktoolClient(selection, commands, {
|
|
environment: {},
|
|
platform: "linux",
|
|
}).catch((cause: unknown) => cause);
|
|
expect((failure as AnalysisTimeoutError).cleanupPrimary).toBeInstanceOf(
|
|
AnalysisTimeoutError,
|
|
);
|
|
expect(projectAnalysisError(failure as AnalysisTimeoutError)).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
});
|
|
expect(owner.stop).not.toHaveBeenCalled();
|
|
await commands.close();
|
|
expect((failure as AnalysisTimeoutError).cleanupIncomplete).toBe(false);
|
|
});
|
|
|
|
it("reports a cancelled version probe as cancelled", async () => {
|
|
vi.mocked(runOwnedCommand).mockRejectedValueOnce(
|
|
new OwnedCommandFailure("cancelled", "Command cancelled."),
|
|
);
|
|
await expect(
|
|
inspectApktoolClient(selection, commands, {
|
|
environment: {},
|
|
platform: "linux",
|
|
}),
|
|
).rejects.toBeInstanceOf(AnalysisCancelledError);
|
|
});
|
|
|
|
it("keeps unresolved probe cleanup typed when the launcher overflows its output", async () => {
|
|
const overflow = () =>
|
|
new OwnedCommandFailure(
|
|
"output-limit",
|
|
"Command diagnostic output exceeded its complete-output budget.",
|
|
null,
|
|
"process group still running",
|
|
undefined,
|
|
["pid:7", "process-group:7"],
|
|
owner,
|
|
);
|
|
vi.mocked(runOwnedCommand).mockRejectedValueOnce(overflow());
|
|
const failure = await inspectApktoolClient(selection, commands, {
|
|
environment: {},
|
|
platform: "linux",
|
|
}).catch((cause: unknown) => cause);
|
|
expect((failure as AnalysisTimeoutError).cleanupResources).toEqual([
|
|
"pid:7",
|
|
"process-group:7",
|
|
]);
|
|
|
|
// Both provider entry points report the unresolved processes as cleanup.
|
|
const provider = new ApktoolProvider({
|
|
environment: { REA_APKTOOL_COMMAND: "/opt/apktool/apktool" },
|
|
});
|
|
vi.mocked(runOwnedCommand).mockRejectedValueOnce(overflow());
|
|
await expect(provider.inspectAvailability()).rejects.toMatchObject({
|
|
cleanupIncomplete: true,
|
|
});
|
|
vi.mocked(runOwnedCommand).mockRejectedValueOnce(overflow());
|
|
const result = await provider.execute({
|
|
operation: "decode_android_resources",
|
|
input: {},
|
|
} as never);
|
|
expect(result.ok ? null : result.error.cleanupIncomplete).toBe(true);
|
|
expect(result.ok ? null : projectAnalysisError(result.error)).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: { resources: ["pid:7", "process-group:7"] },
|
|
});
|
|
await provider.close();
|
|
expect(result.ok ? null : result.error.cleanupIncomplete).toBe(false);
|
|
});
|
|
|
|
it.each(["linux", "win32"] as const)(
|
|
"interprets advisory applicability using the selected %s host",
|
|
async (platform) => {
|
|
vi.mocked(runOwnedCommand).mockImplementationOnce(
|
|
async (_spawn, limits) => {
|
|
const output = {
|
|
stdout: { text: "2.9.2\n" },
|
|
stderr: { text: "" },
|
|
} as Awaited<ReturnType<typeof runOwnedCommand>>;
|
|
limits.observeCompleted?.(output);
|
|
return output;
|
|
},
|
|
);
|
|
const identity = await inspectApktoolClient(selection, commands, {
|
|
environment: {},
|
|
platform,
|
|
});
|
|
expect(identity.version).toBe("2.9.2");
|
|
expect(
|
|
identity.limitations.some((warning) =>
|
|
warning.includes("GHSA-vgwr-4w3p-xmjv"),
|
|
),
|
|
).toBe(platform === "win32");
|
|
},
|
|
);
|