mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
76b56957e9
Reuse validated class pages for inline pagination audits while retaining complete provider replies. Move retention into the shared Android workflow and remove the MCP-only service wrapper and application text reparser. Preserve unknown JSON metadata, late cancellation and retention failures.
157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import {
|
|
parseJadxEnvelope,
|
|
parseJadxClassPage,
|
|
parseJadxJson,
|
|
normalizeJadxText,
|
|
parseJadxFailure,
|
|
} from "../../../src/android/JadxProtocol.js";
|
|
import { androidInputSchemas } from "../../../src/domain/android/androidAnalysis.js";
|
|
|
|
it("recognizes named bridge failures without reinterpreting arbitrary engine text", () => {
|
|
const failure = {
|
|
kind: "jvm-memory-failure",
|
|
memory_area: "heap",
|
|
exception_type: "java.lang.OutOfMemoryError",
|
|
message: "Java heap space",
|
|
max_heap_bytes: 67108864,
|
|
};
|
|
expect(parseJadxFailure(JSON.stringify(failure))).toEqual(failure);
|
|
for (const value of [
|
|
"load failed: cannot read /inputs/Java heap space.apk",
|
|
"{malformed Java heap space",
|
|
JSON.stringify({
|
|
...failure,
|
|
exception_type: "java.lang.IllegalArgumentException",
|
|
}),
|
|
JSON.stringify({ ...failure, max_heap_bytes: -1 }),
|
|
JSON.stringify({ ...failure, memory_area: "native" }),
|
|
JSON.stringify({ ...failure, arbitrary: true }),
|
|
])
|
|
expect(parseJadxFailure(value)).toBeUndefined();
|
|
});
|
|
|
|
it("validates provider text envelopes and rejects unsupported content rather than dropping it", () => {
|
|
expect(
|
|
parseJadxEnvelope(
|
|
{ content: [{ type: "text", text: "reported error" }], isError: true },
|
|
"inspect_android_package",
|
|
),
|
|
).toEqual({
|
|
content: [{ type: "text", text: "reported error" }],
|
|
isError: true,
|
|
});
|
|
for (const response of [
|
|
{ content: [] },
|
|
{ content: [{ type: "image", data: "AA==", mimeType: "image/png" }] },
|
|
{
|
|
content: [
|
|
{ type: "text", text: "first" },
|
|
{ type: "text", text: "second" },
|
|
],
|
|
},
|
|
])
|
|
expect(() =>
|
|
parseJadxEnvelope(response, "inspect_android_package"),
|
|
).toThrowError("unsupported MCP content envelope");
|
|
expect(() =>
|
|
parseJadxJson("{malformed", "inspect_android_package"),
|
|
).toThrowError("malformed JSON text");
|
|
});
|
|
|
|
it("retains UTF-8 source and recognizes only the producer's terminal truncation marker", () => {
|
|
expect(normalizeJadxText("日本語")).toEqual({
|
|
text: "日本語",
|
|
status: "complete",
|
|
reported_total_bytes: null,
|
|
});
|
|
const text = "日本語\n\n... [truncated: exceeds 9 bytes, total 100 bytes]";
|
|
expect(normalizeJadxText(text)).toEqual({
|
|
text,
|
|
status: "partial",
|
|
reported_total_bytes: 100,
|
|
});
|
|
expect(normalizeJadxText(text + "\nmore actual source").status).toBe(
|
|
"complete",
|
|
);
|
|
});
|
|
|
|
it("rejects invalid APK selectors and accepts an explicit zero overload without a confirmation flag", () => {
|
|
const input = {
|
|
path: "/fixture.apk",
|
|
class_name: "fixture.Target",
|
|
method_name: "choose",
|
|
};
|
|
expect(
|
|
androidInputSchemas.inspect_android_method.safeParse({
|
|
...input,
|
|
overload_index: 0,
|
|
}).success,
|
|
).toBe(true);
|
|
expect(
|
|
androidInputSchemas.inspect_android_method.safeParse({
|
|
...input,
|
|
overload_index: -1,
|
|
}).success,
|
|
).toBe(false);
|
|
expect(
|
|
androidInputSchemas.inspect_android_method.safeParse({
|
|
...input,
|
|
overload_index: 0.5,
|
|
}).success,
|
|
).toBe(false);
|
|
expect(
|
|
androidInputSchemas.inspect_android_method.safeParse({
|
|
...input,
|
|
approved: true,
|
|
}).success,
|
|
).toBe(false);
|
|
expect(
|
|
androidInputSchemas.search_android_classes.safeParse({
|
|
path: "/fixture.apk",
|
|
query: "",
|
|
}).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
it("preserves JSON page and envelope annotations while rejecting unsupported inventory scope", () => {
|
|
const page = {
|
|
scope: "top_level",
|
|
total_classes: 1,
|
|
offset: 0,
|
|
next_offset: 1,
|
|
limit: 4096,
|
|
items: ["fixture.Target"],
|
|
audit: { source: "雪", missing: null },
|
|
};
|
|
Object.defineProperty(page, "__proto__", {
|
|
value: { observed: "page" },
|
|
enumerable: true,
|
|
});
|
|
expect(parseJadxClassPage(page)).toEqual(page);
|
|
expect(Object.hasOwn(parseJadxClassPage(page), "__proto__")).toBe(true);
|
|
expect(() => parseJadxClassPage({ ...page, scope: "all_dex" })).toThrow();
|
|
const response = {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: JSON.stringify(page),
|
|
annotations: { audience: ["assistant"] },
|
|
},
|
|
],
|
|
_meta: { page: 1 },
|
|
};
|
|
Object.defineProperty(response, "__proto__", {
|
|
value: { observed: "envelope" },
|
|
enumerable: true,
|
|
});
|
|
Object.defineProperty(response.content[0], "__proto__", {
|
|
value: { observed: "content" },
|
|
enumerable: true,
|
|
});
|
|
const parsed = parseJadxEnvelope(response, "search_android_classes");
|
|
expect(parsed).toEqual(response);
|
|
expect(Object.hasOwn(parsed, "__proto__")).toBe(true);
|
|
expect(Object.hasOwn(parsed.content[0], "__proto__")).toBe(true);
|
|
});
|