Files
rea/tests/boundary/process/androidSearchEvidenceBudget.test.ts
T
akushonkamen 7a0140b4aa fix(android): bound inline search Evidence with retained inventory provenance (#1890)
Focused and zero-match search_android_classes responses embedded every
examined class name: JadxSession records each raw list_classes page into
observed calls, those calls ride into Evidence raw_result on the
registration path, and the complete inline Evidence is serialized into
every tool response (issue #1850: 382,435 bytes for one match).

When the server session retains Evidence, record the complete raw-call
record first, then deliver a summary Evidence that keeps the pagination
audit (totals, offsets, page sizes, elided item counts), the unchanged
normalized matches and truthful coverage metadata, and links the
retained complete inventory by Evidence ID. Sessions without retention
keep the complete inline delivery, so complete CLI JSON output is
unchanged and no provenance is discarded.

🤖 Generated with AI assistance (disclosure voluntary per repo policy)
2026-10-12 01:07:22 +08:00

132 lines
4.5 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);
// The complete inventory stays locally retrievable through the retained 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(INVENTORY_CLASS_COUNT);
expect(JSON.stringify(complete?.raw_result)).toContain("fixture.Class7094");
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 the complete search inventory 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 JSON output keeps the full raw provider inventory.
expect(rawInventoryNames(result.value.raw_result)).toBe(
INVENTORY_CLASS_COUNT,
);
await provider.close();
await verifyJadxFixtureCleanup(launches);
});