mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 21:36:13 +00:00
Extend static manifest declarations and retain precise source locations and historical unknowns. Share bounded selected component views across CLI and MCP with complete parent Evidence. Verified real ApiDemos, an AAPT2-built fixture, legacy Evidence, default-package metadata and relevant CLI/MCP contracts. Recorded whole-task token estimates, timing/resource limits and independent main-failure controls in the PR. Co-authored-by: Harsh <johiyaharsh@gmail.com>
274 lines
7.6 KiB
TypeScript
274 lines
7.6 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { createEvidence } from "../evidence.js";
|
|
import type { JsonValue } from "../jsonValue.js";
|
|
import {
|
|
analysisViewRequestSchema,
|
|
androidComponentsViewResultSchema,
|
|
androidComponentSummaryViewResultSchema,
|
|
projectAnalysisView,
|
|
type AnalysisViewParent,
|
|
} from "./analysisView.js";
|
|
|
|
const component = (name: string, extra: Record<string, JsonValue> = {}) => ({
|
|
kind: "activity",
|
|
declared_name: name,
|
|
name,
|
|
exported: null,
|
|
exported_default: null,
|
|
permission: null,
|
|
permission_source: "none",
|
|
intent_filter_count: 0,
|
|
actions: [] as string[],
|
|
categories: [],
|
|
foreground_service_type: null,
|
|
authorities: null,
|
|
target_activity: null,
|
|
...extra,
|
|
});
|
|
const report = (
|
|
items = [
|
|
component("example.One", {
|
|
declared_name: ".One",
|
|
exported: true,
|
|
actions: ["VIEW"],
|
|
declaration_index: 10,
|
|
declaration_coverage: "complete",
|
|
}),
|
|
component("example.Boot", {
|
|
kind: "receiver",
|
|
exported_default: true,
|
|
actions: ["BOOT"],
|
|
declaration_index: 20,
|
|
}),
|
|
component("example.One", {
|
|
declared_name: ".One",
|
|
exported: false,
|
|
actions: ["VIEW"],
|
|
declaration_index: 30,
|
|
declaration_coverage: "partial",
|
|
}),
|
|
],
|
|
) => ({
|
|
engine: {
|
|
name: "jadx",
|
|
version: null,
|
|
artifact_sha256: "e".repeat(64),
|
|
source_revision: null,
|
|
worker_count: 1,
|
|
heap_limit_mib: null,
|
|
},
|
|
package_name: "example",
|
|
version_name: null,
|
|
version_code: null,
|
|
min_sdk: null,
|
|
target_sdk: "35",
|
|
permissions: [],
|
|
manifest: {
|
|
status: "complete",
|
|
text: "<manifest/>",
|
|
reported_total_bytes: 11,
|
|
},
|
|
components: {
|
|
status: "partial",
|
|
items,
|
|
limitations: ["Producer component limit"],
|
|
},
|
|
class_count: 3,
|
|
resource_count: 0,
|
|
signature_verification: "not_performed",
|
|
});
|
|
const parent = (value = report()): AnalysisViewParent => {
|
|
const evidence = createEvidence(
|
|
{ path: "/retained.apk", sha256: "a".repeat(64), format: "apk" },
|
|
{ id: "jadx", name: "JADX", version: "0.7.1" },
|
|
{
|
|
operation: "inspect_android_package",
|
|
parameters: {},
|
|
result: value,
|
|
limitations: ["Parent condition"],
|
|
},
|
|
);
|
|
return {
|
|
evidenceId: evidence.evidence_id,
|
|
operation: evidence.operation,
|
|
normalizedResult: value,
|
|
subject: evidence.subject,
|
|
limitations: evidence.limitations,
|
|
};
|
|
};
|
|
const project = (source: AnalysisViewParent, view: unknown) => {
|
|
const result = projectAnalysisView(
|
|
source,
|
|
analysisViewRequestSchema.parse(view),
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
return result.value;
|
|
};
|
|
|
|
it("keeps parent ordinals, duplicate declarations and coverage after filtering, paging and field selection", () => {
|
|
const source = parent();
|
|
const before = JSON.stringify(source);
|
|
const page = androidComponentsViewResultSchema.parse(
|
|
project(source, {
|
|
kind: "android",
|
|
facet: "components",
|
|
names: [".One"],
|
|
actions: ["VIEW"],
|
|
kinds: ["activity"],
|
|
offset: 1,
|
|
limit: 1,
|
|
fields: ["name", "declaration_index"],
|
|
}),
|
|
);
|
|
expect(page.items).toEqual([
|
|
{
|
|
source_index: 2,
|
|
name: "example.One",
|
|
declaration_index: 30,
|
|
declaration_coverage: "partial",
|
|
},
|
|
]);
|
|
expect(page.coverage).toMatchObject({
|
|
total: 2,
|
|
examined: 1,
|
|
exhausted: true,
|
|
});
|
|
expect(page.source_components).toBe(3);
|
|
expect(page.source_status).toBe("partial");
|
|
expect(page.artifact).toEqual({
|
|
path: "/retained.apk",
|
|
sha256: "a".repeat(64),
|
|
});
|
|
expect(page.parent_evidence_id).toBe(source.evidenceId);
|
|
expect(page.limitations).toEqual(
|
|
expect.arrayContaining(["Parent condition", "Producer component limit"]),
|
|
);
|
|
expect(JSON.stringify(source)).toBe(before);
|
|
});
|
|
|
|
it("keeps omitted historical facts unknown even with empty or expanded field selections", () => {
|
|
const source = parent(report([component("legacy.Class")]));
|
|
for (const fields of [[], ["enabled", "intent_filters", "location"]]) {
|
|
const page = androidComponentsViewResultSchema.parse(
|
|
project(source, { kind: "android", facet: "components", fields }),
|
|
);
|
|
expect(page.items).toEqual([
|
|
{ source_index: 0, declaration_coverage: "unknown" },
|
|
]);
|
|
}
|
|
});
|
|
|
|
it("counts explicit exported declarations separately from defaults and unknown historical coverage", () => {
|
|
const source = parent();
|
|
const summary = androidComponentSummaryViewResultSchema.parse(
|
|
project(source, { kind: "android", facet: "component-summary" }),
|
|
);
|
|
expect(summary.summary).toEqual({
|
|
matching_components: 3,
|
|
by_kind: {
|
|
activity: 2,
|
|
"activity-alias": 0,
|
|
service: 0,
|
|
receiver: 1,
|
|
provider: 0,
|
|
},
|
|
exported_declarations: { true: 1, false: 1, unknown: 1 },
|
|
exported_defaults: { true: 1, false: 0, unknown: 2 },
|
|
declaration_coverage: { complete: 1, partial: 1, unknown: 1 },
|
|
});
|
|
const explicit = androidComponentsViewResultSchema.parse(
|
|
project(source, {
|
|
kind: "android",
|
|
facet: "components",
|
|
exported: true,
|
|
fields: [],
|
|
}),
|
|
);
|
|
expect(explicit.items.map((row) => row.source_index)).toEqual([0]);
|
|
const unknown = androidComponentSummaryViewResultSchema.parse(
|
|
project(source, {
|
|
kind: "android",
|
|
facet: "component-summary",
|
|
exported: null,
|
|
}),
|
|
);
|
|
expect(unknown.summary.matching_components).toBe(1);
|
|
expect(unknown.summary.exported_defaults.true).toBe(1);
|
|
});
|
|
|
|
it("preserves partial source coverage on empty filtered pages and never turns empty filters into all rows", () => {
|
|
for (const selection of [
|
|
{ actions: [] },
|
|
{ names: [] },
|
|
{ kinds: [] },
|
|
{ actions: ["absent"] },
|
|
]) {
|
|
const page = androidComponentsViewResultSchema.parse(
|
|
project(parent(), { kind: "android", facet: "components", ...selection }),
|
|
);
|
|
expect(page.items).toEqual([]);
|
|
expect(page.coverage.total).toBe(0);
|
|
expect(page.source_status).toBe("partial");
|
|
expect(page.unknowns.join(" ")).toContain("does not establish absence");
|
|
}
|
|
});
|
|
|
|
it("bounds selected row serialization and lets field selection recover without changing the source", () => {
|
|
const source = parent(
|
|
report([
|
|
component("large.Class", { process: "x".repeat(2 * 1024 * 1024) }),
|
|
]),
|
|
);
|
|
expect(
|
|
projectAnalysisView(
|
|
source,
|
|
analysisViewRequestSchema.parse({ kind: "android", facet: "components" }),
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisResourceConstraintError" },
|
|
});
|
|
const small = androidComponentsViewResultSchema.parse(
|
|
project(source, { kind: "android", facet: "components", fields: ["name"] }),
|
|
);
|
|
expect(small.items).toEqual([
|
|
{ source_index: 0, name: "large.Class", declaration_coverage: "unknown" },
|
|
]);
|
|
});
|
|
|
|
it("returns a resumable byte-limited page instead of silently omitting later rows", () => {
|
|
const source = parent(
|
|
report(
|
|
Array.from({ length: 3 }, (_, index) =>
|
|
component(`large.${index}`, { process: "x".repeat(400_000) }),
|
|
),
|
|
),
|
|
);
|
|
const first = androidComponentsViewResultSchema.parse(
|
|
project(source, {
|
|
kind: "android",
|
|
facet: "components",
|
|
limit: 3,
|
|
fields: ["process"],
|
|
}),
|
|
);
|
|
expect(first.byte_limited).toBe(true);
|
|
expect(first.items.map((row) => row.source_index)).toEqual([0, 1]);
|
|
expect(first.coverage).toMatchObject({
|
|
total: 3,
|
|
next_offset: 2,
|
|
exhausted: false,
|
|
});
|
|
const last = androidComponentsViewResultSchema.parse(
|
|
project(source, {
|
|
kind: "android",
|
|
facet: "components",
|
|
offset: 2,
|
|
limit: 3,
|
|
fields: ["process"],
|
|
}),
|
|
);
|
|
expect(last.items.map((row) => row.source_index)).toEqual([2]);
|
|
expect(last.coverage.exhausted).toBe(true);
|
|
});
|