Files
rea/tests/boundary/process/androidSearchEvidenceBudget.test.ts
2d84c800bc perf(android): filter class pages before bridge serialization (#4791)
Filter class names at the native owner before serialization, and resume matching pages through contiguous inventory positions without a second index or query-result cache. Preserve selected source Evidence, query identity, complete top-level coverage and shared CLI/MCP behavior.

Fixes #4776.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 13:21:37 +00:00

150 lines
5.2 KiB
TypeScript

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it as test, onTestFinished } from "vitest";
import { androidResultSchemas } from "../../../src/domain/android/androidAnalysis.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import {
createJadxProtocolFixture,
verifyJadxFixtureCleanup,
} from "../../fixtures/android/jadx.js";
const INVENTORY_CLASS_COUNT = 7096;
/** Count inventoried names across raw call records: MCP pages ride in text content. */
const rawInventoryNames = (raw: unknown): number => {
const calls =
(
raw as {
calls?: {
operation: string;
response: { content?: { text?: string }[] };
}[];
}
).calls ?? [];
return calls
.flatMap((call) => call.response.content ?? [])
.map((entry) =>
entry.text === undefined ? {} : (JSON.parse(entry.text) as unknown),
)
.filter((page) => Array.isArray((page as { items?: unknown[] }).items))
.flatMap((page) => (page as { items: unknown[] }).items).length;
};
test("delivers bounded search_android_classes Evidence while retaining the complete inventory", async () => {
const { provider, apk, launches } = await createJadxProtocolFixture(
"normal",
undefined,
{ classCount: INVENTORY_CLASS_COUNT - 1 },
);
const session = createTestBinarySession(() => {
throw new Error("APK analysis must not acquire a deep binary provider");
});
const server = createServer(
{ kind: "session", session },
{ androidAnalysis: provider },
);
const client = new Client({
name: "android-search-inventory-budget",
version: "1",
});
onTestFinished(async () => {
await client.close();
await server.close();
await session.close();
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const search = async (query: string) =>
parseEvidence(
(
await client.callTool({
name: "search_android_classes",
arguments: { path: apk, query },
})
).structuredContent,
);
const single = await search("Target");
expect(
androidResultSchemas.search_android_classes.parse(single.normalized_result),
).toMatchObject({
query: "Target",
total_classes: INVENTORY_CLASS_COUNT,
matches: ["fixture.Target"],
coverage: "complete",
});
// The inline raw record keeps the pagination audit, not the class names.
const singleRaw = JSON.stringify(single.raw_result);
expect(singleRaw).not.toContain("fixture.Class");
expect(singleRaw).toContain("items_omitted");
expect(Buffer.byteLength(JSON.stringify(single))).toBeLessThan(32 * 1024);
// Every matching producer row remains in the retained source record.
const completeId = single.evidence_links[0];
expect(completeId).toBeDefined();
expect(completeId).toBe(
(single.raw_result as { complete_evidence_id: string })
.complete_evidence_id,
);
const complete = session.evidenceById(completeId as string);
expect(complete).toBeDefined();
expect(complete?.operation).toBe("search_android_classes");
expect(rawInventoryNames(complete?.raw_result)).toBe(1);
expect(JSON.stringify(complete?.raw_result)).toContain("fixture.Target");
expect(JSON.stringify(complete?.raw_result)).not.toContain("fixture.Class");
// Selection precedes paging: a match beyond the first unfiltered page is
// still found, and the complete inventory remains explicitly available.
const last = await search("Class7094");
expect(last.normalized_result).toMatchObject({
total_classes: INVENTORY_CLASS_COUNT,
matches: ["fixture.Class7094"],
coverage: "complete",
});
const inventory = await search("");
const inventorySource = session.evidenceById(
inventory.evidence_links[0] as string,
);
expect(rawInventoryNames(inventorySource?.raw_result)).toBe(
INVENTORY_CLASS_COUNT,
);
const empty = await search("fixture.Missing");
expect(
androidResultSchemas.search_android_classes.parse(empty.normalized_result),
).toMatchObject({
query: "fixture.Missing",
total_classes: INVENTORY_CLASS_COUNT,
matches: [],
coverage: "complete",
});
expect(JSON.stringify(empty.raw_result)).not.toContain("fixture.Class");
await client.close();
await server.close();
await verifyJadxFixtureCleanup(launches);
});
test("keeps complete matching producer rows inline without a retaining session", async () => {
const { service, apk, launches, provider } = await createJadxProtocolFixture(
"normal",
undefined,
{ classCount: INVENTORY_CLASS_COUNT - 1 },
);
const result = await service.execute("search_android_classes", {
path: apk,
query: "Target",
});
if (!result.ok) throw result.error;
// Complete CLI Evidence keeps every matching row, without unrelated names.
expect(rawInventoryNames(result.value.raw_result)).toBe(1);
expect(JSON.stringify(result.value.raw_result)).not.toContain(
"fixture.Class",
);
await provider.close();
await verifyJadxFixtureCleanup(launches);
});