mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(jeb): cache parsed metadata independently of optional version
Own the parsed engine facts separately from identity projections. Reuse successful unknown-version metadata, retain explicit refresh, and bind each execution identity to its own selected metadata under concurrent inspection. Refs #4600 and #4597. Six affected files pass 58 tests; static, documentation and discovery checks passed. Real JEB and reconnect behavior remain unverified; prepared reconnect PR3754 stays separate pending cleanup ownership work.
This commit is contained in:
@@ -52,6 +52,7 @@ All four are read-only with respect to the target file; only `open_jeb_project`
|
||||
- The engine caps `list_units` pages at 100; a full page is reported as `coverage: "partial"` because more units may exist. An empty remainder is `complete` only when fewer units than requested were returned.
|
||||
- When `decompile_jeb_item` omits `unit_path`, the engine implicitly selects the project's first code unit; REA reports `unit_path: null` and a limitation instead of guessing the effective unit.
|
||||
- Engine refusals (`success: false` envelopes) are preserved as unsupported-target errors carrying the engine's own message, for example a nonexistent item address.
|
||||
- Successfully parsed client information is reused for the lifetime of the MCP connection, including when the engine omits its version. Explicit client inspection and availability probes refresh it.
|
||||
- An unreachable endpoint is a provider-availability failure with recovery guidance; REA does not retry or spawn the engine.
|
||||
|
||||
Real-engine verification lives in the JEB lane described in [testing.md](testing.md); mock transport tests do not establish real-engine compatibility.
|
||||
|
||||
@@ -409,3 +409,76 @@ it("forwards in-flight cancellation to the MCP call", async () => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
});
|
||||
|
||||
it("refreshes cached unknown metadata only through an explicit information probe", async () => {
|
||||
const responses: Record<string, JsonValue> = {
|
||||
get_client_information: { success: true, gui_client: false },
|
||||
list_units: { success: true, units: [] },
|
||||
};
|
||||
const connection = scriptedConnection(responses);
|
||||
const provider = providerWith(connection);
|
||||
try {
|
||||
const first = await provider.execute({
|
||||
operation: "list_jeb_units",
|
||||
input: { count: 2, index: 0 },
|
||||
});
|
||||
if (!first.ok) throw first.error;
|
||||
expect(first.value.provider.version).toBeNull();
|
||||
responses.get_client_information = {
|
||||
success: true,
|
||||
version: "5.49.0",
|
||||
gui_client: true,
|
||||
};
|
||||
const inspected = await provider.execute({
|
||||
operation: "inspect_jeb_client",
|
||||
input: {},
|
||||
});
|
||||
if (!inspected.ok) throw inspected.error;
|
||||
const after = await provider.execute({
|
||||
operation: "list_jeb_units",
|
||||
input: { count: 2, index: 0 },
|
||||
});
|
||||
if (!after.ok) throw after.error;
|
||||
expect(after.value.provider.version).toBe("5.49.0");
|
||||
expect(after.value.result).toMatchObject({
|
||||
engine: { version: "5.49.0", gui_client: true },
|
||||
});
|
||||
expect(connection.calls.map(([name]) => name)).toEqual([
|
||||
"list_units",
|
||||
"get_client_information",
|
||||
"get_client_information",
|
||||
"list_units",
|
||||
]);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps concurrent client inspections bound to their own parsed metadata", async () => {
|
||||
const connection = scriptedConnection({});
|
||||
let replies = 0;
|
||||
connection.call = async () => ({
|
||||
success: true,
|
||||
version: `inspection-${++replies}`,
|
||||
gui_client: replies === 1,
|
||||
});
|
||||
const provider = providerWith(connection);
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
provider.execute({ operation: "inspect_jeb_client", input: {} }),
|
||||
provider.execute({ operation: "inspect_jeb_client", input: {} }),
|
||||
]);
|
||||
for (const [index, result] of results.entries()) {
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.provider.version).toBe(`inspection-${index + 1}`);
|
||||
expect(result.value.result).toMatchObject({
|
||||
engine: {
|
||||
version: result.value.provider.version,
|
||||
gui_client: index === 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
+27
-21
@@ -105,8 +105,16 @@ const refusalReason = (payload: unknown, fallback: string): string => {
|
||||
export class JebProvider implements JebAnalysisPort {
|
||||
readonly #connection: JebMcpConnection;
|
||||
readonly #endpoint: URL;
|
||||
#identity: ProviderIdentity = JEB_PROVIDER_IDENTITY;
|
||||
#guiClient: boolean | null = null;
|
||||
#clientInformation:
|
||||
| Pick<z.infer<typeof clientInformation>, "version" | "gui_client">
|
||||
| undefined;
|
||||
|
||||
#identity(version: string | null | undefined): ProviderIdentity {
|
||||
return {
|
||||
...JEB_PROVIDER_IDENTITY,
|
||||
version: version ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(options: JebProviderOptions = {}) {
|
||||
const environment = options.environment ?? {};
|
||||
@@ -128,7 +136,10 @@ export class JebProvider implements JebAnalysisPort {
|
||||
): Promise<JebAnalysisAvailability> {
|
||||
try {
|
||||
const tools = new Set(await this.#connection.listTools(signal));
|
||||
await this.#refreshClientInformation("inspect_jeb_client", signal);
|
||||
const { information } = await this.#refreshClientInformation(
|
||||
"inspect_jeb_client",
|
||||
signal,
|
||||
);
|
||||
return {
|
||||
status: "available",
|
||||
code: null,
|
||||
@@ -138,7 +149,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
).filter((operation) => tools.has(JEB_ENGINE_TOOLS[operation])),
|
||||
diagnostics: {
|
||||
endpoint: this.#endpoint.toString(),
|
||||
engine_version: this.#identity.version,
|
||||
engine_version: information.version ?? null,
|
||||
},
|
||||
};
|
||||
} catch (cause) {
|
||||
@@ -169,11 +180,6 @@ export class JebProvider implements JebAnalysisPort {
|
||||
const execution = await this.#dispatch(request, options?.signal);
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(request.operation));
|
||||
if (execution.ok)
|
||||
this.#identity = {
|
||||
...this.#identity,
|
||||
version: execution.value.provider.version,
|
||||
};
|
||||
return execution;
|
||||
} catch (cause) {
|
||||
if (options?.signal?.aborted)
|
||||
@@ -218,13 +224,14 @@ export class JebProvider implements JebAnalysisPort {
|
||||
readonly endpoint: string;
|
||||
readonly gui_client: boolean | null;
|
||||
}> {
|
||||
if (this.#identity.version === null)
|
||||
await this.#refreshClientInformation(operation, signal);
|
||||
const information =
|
||||
this.#clientInformation ??
|
||||
(await this.#refreshClientInformation(operation, signal)).information;
|
||||
return {
|
||||
name: "jeb",
|
||||
version: this.#identity.version,
|
||||
version: information.version ?? null,
|
||||
endpoint: this.#endpoint.toString(),
|
||||
gui_client: this.#guiClient,
|
||||
gui_client: information.gui_client ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -242,11 +249,10 @@ export class JebProvider implements JebAnalysisPort {
|
||||
const information = clientInformation.parse(
|
||||
this.#requireSuccess(payload, operation),
|
||||
);
|
||||
this.#identity = {
|
||||
...this.#identity,
|
||||
version: information.version ?? null,
|
||||
this.#clientInformation = {
|
||||
version: information.version,
|
||||
gui_client: information.gui_client,
|
||||
};
|
||||
this.#guiClient = information.gui_client ?? null;
|
||||
return { information, payload };
|
||||
}
|
||||
|
||||
@@ -266,7 +272,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
message: information.message ?? null,
|
||||
});
|
||||
return ok(
|
||||
createAnalysisExecution(result, this.#identity, {
|
||||
createAnalysisExecution(result, this.#identity(information.version), {
|
||||
rawResult: payload,
|
||||
limitations:
|
||||
information.version === undefined
|
||||
@@ -299,7 +305,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
units: opened.units,
|
||||
});
|
||||
return ok(
|
||||
createAnalysisExecution(result, this.#identity, {
|
||||
createAnalysisExecution(result, this.#identity(engine.version), {
|
||||
rawResult: payload,
|
||||
limitations: [
|
||||
"JEB resolves and analyzes the artifact on the engine host; REA did not open the target itself.",
|
||||
@@ -338,7 +344,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
coverage: units.length < count ? "complete" : "partial",
|
||||
});
|
||||
return ok(
|
||||
createAnalysisExecution(result, this.#identity, {
|
||||
createAnalysisExecution(result, this.#identity(engine.version), {
|
||||
rawResult: payload,
|
||||
limitations: [
|
||||
"The engine caps unit pages at 100; a full page means more units may exist.",
|
||||
@@ -373,7 +379,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
text: decompiled.text,
|
||||
});
|
||||
return ok(
|
||||
createAnalysisExecution(result, this.#identity, {
|
||||
createAnalysisExecution(result, this.#identity(engine.version), {
|
||||
rawResult: payload,
|
||||
limitations:
|
||||
unit_path === undefined
|
||||
|
||||
@@ -296,30 +296,40 @@ it("keeps the MCP server usable when REA_JEB_MCP_URL is invalid", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ operation: "list_jeb_units", tool: "list_units", input: { count: 2 } },
|
||||
{
|
||||
operation: "decompile_jeb_item",
|
||||
tool: "decompile_code_item",
|
||||
input: {
|
||||
item_address: "LSample;->value()I",
|
||||
item_kind: "method",
|
||||
unit_path: "sample.dex",
|
||||
it.each(
|
||||
[
|
||||
{ operation: "list_jeb_units", tool: "list_units", input: { count: 2 } },
|
||||
{
|
||||
operation: "decompile_jeb_item",
|
||||
tool: "decompile_code_item",
|
||||
input: {
|
||||
item_address: "LSample;->value()I",
|
||||
item_kind: "method",
|
||||
unit_path: "sample.dex",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "open_jeb_project",
|
||||
tool: "open_project",
|
||||
input: { path: "sample.apk" },
|
||||
},
|
||||
])(
|
||||
"attributes auxiliary metadata refusal to requested $operation through MCP",
|
||||
async ({ operation, tool, input }) => {
|
||||
const fixture = await startJebMcpFixture((name) =>
|
||||
name === "get_client_information"
|
||||
{
|
||||
operation: "open_jeb_project",
|
||||
tool: "open_project",
|
||||
input: { path: "sample.apk" },
|
||||
},
|
||||
].flatMap((request) =>
|
||||
["refusal", "unknown-version"].map((metadata) => ({
|
||||
...request,
|
||||
metadata,
|
||||
})),
|
||||
),
|
||||
)(
|
||||
"preserves requested $operation through MCP with metadata $metadata",
|
||||
async ({ operation, tool, input, metadata }) => {
|
||||
let informationCalls = 0;
|
||||
const fixture = await startJebMcpFixture((name) => {
|
||||
if (name !== "get_client_information") return jebPrimaryResponses[name]!;
|
||||
informationCalls++;
|
||||
return metadata === "refusal" || informationCalls > 1
|
||||
? { success: false, message: "Client information temporarily refused" }
|
||||
: jebPrimaryResponses[name]!,
|
||||
);
|
||||
: { success: true, gui_client: false };
|
||||
});
|
||||
const provider = new JebProvider({
|
||||
environment: { REA_JEB_MCP_URL: fixture.endpoint },
|
||||
});
|
||||
@@ -336,24 +346,42 @@ it.each([
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const failed = await client.callTool({
|
||||
name: operation,
|
||||
arguments: input,
|
||||
});
|
||||
expect(failed.isError).toBe(true);
|
||||
const text = failed.content
|
||||
.map((block) => (block.type === "text" ? block.text : ""))
|
||||
.join("");
|
||||
expect(JSON.parse(text)).toMatchObject({
|
||||
error: {
|
||||
code: "unsupported_target",
|
||||
details: {
|
||||
operation,
|
||||
reason: "Client information temporarily refused",
|
||||
const call = () => client.callTool({ name: operation, arguments: input });
|
||||
if (metadata === "unknown-version") {
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const called = await call();
|
||||
expect(called.isError).not.toBe(true);
|
||||
const text = called.content
|
||||
.map((block) => (block.type === "text" ? block.text : ""))
|
||||
.join("");
|
||||
const evidence = parseEvidence(JSON.parse(text));
|
||||
expect(evidence.provider.version).toBeNull();
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
engine: { version: null, gui_client: false },
|
||||
});
|
||||
expect(evidence.raw_result).toEqual(jebPrimaryResponses[tool]);
|
||||
}
|
||||
expect(fixture.calls).toEqual([tool, "get_client_information", tool]);
|
||||
} else {
|
||||
const failed = await client.callTool({
|
||||
name: operation,
|
||||
arguments: input,
|
||||
});
|
||||
expect(failed.isError).toBe(true);
|
||||
const text = failed.content
|
||||
.map((block) => (block.type === "text" ? block.text : ""))
|
||||
.join("");
|
||||
expect(JSON.parse(text)).toMatchObject({
|
||||
error: {
|
||||
code: "unsupported_target",
|
||||
details: {
|
||||
operation,
|
||||
reason: "Client information temporarily refused",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(fixture.calls).toEqual([tool, "get_client_information"]);
|
||||
});
|
||||
expect(fixture.calls).toEqual([tool, "get_client_information"]);
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
await server.close();
|
||||
|
||||
Reference in New Issue
Block a user