mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(jeb): retain selected inputs and completed primary replies
This commit is contained in:
1 parent
3e112595da
commit
0f5ece1d58
3 files changed
+196
-13
No files matched your search
@@ -68,3 +68,10 @@ All four preserve target-file bytes. `open_jeb_project` selects or creates an en
|
||||
- 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.
|
||||
|
||||
A project-open refusal retains the selected engine-host artifact path separately
|
||||
from the service endpoint. Discovery of an unreachable service includes the same
|
||||
start/configuration recovery as a direct request. If a primary reply completes
|
||||
before a later client-information lookup fails or is cancelled, the typed failure
|
||||
retains that reply as partial Evidence. Engine metadata stays unknown; this is not
|
||||
a claim that the full operation completed.
|
||||
+122
-2
@@ -13,8 +13,13 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisTimeoutError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { jebResultSchemas } from "../domain/jeb/jebAnalysis.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import { parseEvidence } from "../domain/evidence.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
jebRequestSchema,
|
||||
jebResultSchemas,
|
||||
} from "../domain/jeb/jebAnalysis.js";
|
||||
|
||||
const scriptedConnection = (
|
||||
responses: Readonly<Record<string, JsonValue | Error>>,
|
||||
@@ -155,6 +160,8 @@ describe("JebProvider observations and failures", () => {
|
||||
);
|
||||
const availability = await provider.inspectAvailability();
|
||||
expect(availability.status).toBe("unavailable");
|
||||
expect(availability.reason).toContain("Start a JEB client serving MCP");
|
||||
expect(availability.reason).toContain("REA_JEB_MCP_URL");
|
||||
const result = await provider.execute({
|
||||
operation: "list_jeb_units",
|
||||
input: { count: 10, index: 0 },
|
||||
@@ -520,3 +527,116 @@ it("preserves provider-authored timeout guidance when projecting the requested o
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("project-open refusal retains the selected engine-host artifact path", async () => {
|
||||
const provider = providerWith(
|
||||
scriptedConnection({
|
||||
open_project: {
|
||||
success: false,
|
||||
message: "The input file could not be processed",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = await provider.execute({
|
||||
operation: "open_jeb_project",
|
||||
input: { path: "/targets/Example.apk" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Refused artifact accepted");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "unsupported_target",
|
||||
details: {
|
||||
path: "/targets/Example.apk",
|
||||
reason: "The input file could not be processed",
|
||||
},
|
||||
});
|
||||
await provider.close();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
operation: "list_jeb_units",
|
||||
input: {},
|
||||
tool: "list_units",
|
||||
payload: {
|
||||
success: true,
|
||||
units: [{ unit_path: "sample.dex", unit_type: "dex" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "open_jeb_project",
|
||||
input: { path: "/targets/Example.apk" },
|
||||
tool: "open_project",
|
||||
payload: {
|
||||
success: true,
|
||||
name: "Example",
|
||||
input_files: [],
|
||||
units: [{ unit_path: "sample.dex", unit_type: "dex" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "decompile_jeb_item",
|
||||
input: {
|
||||
item_kind: "type",
|
||||
item_address: "LExample;",
|
||||
unit_path: "sample.dex",
|
||||
},
|
||||
tool: "decompile_code_item",
|
||||
payload: { success: true, text: "class Example {}" },
|
||||
},
|
||||
])(
|
||||
"retains completed $operation output when later client metadata is cancelled",
|
||||
async ({ operation, input, tool, payload }) => {
|
||||
const request = jebRequestSchema.parse({ operation, input });
|
||||
const controller = new AbortController();
|
||||
const reason = { source: "metadata-cancellation" };
|
||||
const primary = jsonValueSchema.parse(payload);
|
||||
const connection = scriptedConnection({ [tool]: primary });
|
||||
const call = connection.call.bind(connection);
|
||||
connection.call = async (name, args, signal) => {
|
||||
if (name === "get_client_information") {
|
||||
controller.abort(reason);
|
||||
throw new AnalysisCancelledError(name, { cause: reason });
|
||||
}
|
||||
return call(name, args, signal);
|
||||
};
|
||||
const provider = providerWith(connection);
|
||||
const result = await provider.execute(request, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Cancelled metadata lookup accepted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
operation,
|
||||
cause: reason,
|
||||
});
|
||||
const partial = parseEvidence(result.error.partialObservation);
|
||||
expect(partial.operation).toBe(operation);
|
||||
expect(partial.parameters).toEqual(request.input);
|
||||
expect(partial.normalized_result).toMatchObject({
|
||||
status: "partial",
|
||||
observation: payload,
|
||||
engine_metadata: null,
|
||||
});
|
||||
expect(partial.raw_result).toEqual(payload);
|
||||
await provider.close();
|
||||
const control = providerWith(
|
||||
scriptedConnection({
|
||||
[tool]: primary,
|
||||
get_client_information: {
|
||||
success: true,
|
||||
version: "5.48.0",
|
||||
gui_client: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const completed = await control.execute(request);
|
||||
expect(completed.ok).toBe(true);
|
||||
if (!completed.ok) throw completed.error;
|
||||
expect(completed.value.result).toMatchObject({
|
||||
engine: { version: "5.48.0" },
|
||||
});
|
||||
await control.close();
|
||||
},
|
||||
);
|
||||
+67
-11
@@ -2,6 +2,8 @@ import {
|
||||
JebMcpAcquisitionCleanupFailure,
|
||||
JebMcpPhysicalFailure,
|
||||
} from "./JebMcpConnectionOwner.js";
|
||||
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { z } from "zod";
|
||||
import type {
|
||||
@@ -36,6 +38,8 @@ import {
|
||||
type JebMcpConnectionFactory,
|
||||
} from "./JebMcpConnection.js";
|
||||
|
||||
const JEB_CONNECTION_REMEDIATION = `Start a JEB client serving MCP (default ${DEFAULT_JEB_MCP_ENDPOINT}) or set REA_JEB_MCP_URL to the running endpoint.`;
|
||||
|
||||
export const JEB_PROVIDER_IDENTITY: ProviderIdentity = {
|
||||
id: "jeb",
|
||||
name: "JEB",
|
||||
@@ -171,10 +175,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
return {
|
||||
status: "unavailable",
|
||||
code: "not_configured",
|
||||
reason:
|
||||
cause instanceof Error
|
||||
? cause.message
|
||||
: "The JEB MCP endpoint did not respond",
|
||||
reason: `${cause instanceof Error ? cause.message : "The JEB MCP endpoint did not respond"}. ${JEB_CONNECTION_REMEDIATION}`,
|
||||
diagnostics: { endpoint: this.#endpoint.toString() },
|
||||
};
|
||||
}
|
||||
@@ -250,6 +251,42 @@ export class JebProvider implements JebAnalysisPort {
|
||||
};
|
||||
}
|
||||
|
||||
async #engineForObservation(
|
||||
request: JebRequest,
|
||||
payload: JsonValue,
|
||||
observation: unknown,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
try {
|
||||
return await this.#engine(request.operation, signal);
|
||||
} catch (cause: unknown) {
|
||||
throw this.#providerFailure(
|
||||
request.operation,
|
||||
cause,
|
||||
).retainPartialObservation(
|
||||
createAnalysisExecutionEvidence(
|
||||
request.operation,
|
||||
request.input,
|
||||
createAnalysisExecution(
|
||||
{
|
||||
status: "partial",
|
||||
observation: jsonValueSchema.parse(observation),
|
||||
engine_metadata: null,
|
||||
},
|
||||
JEB_PROVIDER_IDENTITY,
|
||||
{
|
||||
rawResult: payload,
|
||||
limitations: [
|
||||
"The primary engine reply completed; client metadata was not obtained, so the full operation result could not be assembled.",
|
||||
],
|
||||
},
|
||||
),
|
||||
"observed",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async #refreshClientInformation(
|
||||
operation: JebOperation,
|
||||
signal: AbortSignal | undefined,
|
||||
@@ -309,9 +346,14 @@ export class JebProvider implements JebAnalysisPort {
|
||||
signal,
|
||||
);
|
||||
const opened = openProject.parse(
|
||||
this.#requireSuccess(payload, request.operation),
|
||||
this.#requireSuccess(payload, request.operation, request.input.path),
|
||||
);
|
||||
const engine = await this.#engineForObservation(
|
||||
request,
|
||||
payload,
|
||||
opened,
|
||||
signal,
|
||||
);
|
||||
const engine = await this.#engine(request.operation, signal);
|
||||
const result = jebResultSchemas.open_jeb_project.parse({
|
||||
engine,
|
||||
project_name: opened.name ?? null,
|
||||
@@ -348,7 +390,12 @@ export class JebProvider implements JebAnalysisPort {
|
||||
this.#requireSuccess(payload, request.operation),
|
||||
);
|
||||
const units = listed.units;
|
||||
const engine = await this.#engine(request.operation, signal);
|
||||
const engine = await this.#engineForObservation(
|
||||
request,
|
||||
payload,
|
||||
listed,
|
||||
signal,
|
||||
);
|
||||
const result = jebResultSchemas.list_jeb_units.parse({
|
||||
engine,
|
||||
filter: filter ?? null,
|
||||
@@ -385,7 +432,12 @@ export class JebProvider implements JebAnalysisPort {
|
||||
const decompiled = decompiledItem.parse(
|
||||
this.#requireSuccess(payload, request.operation),
|
||||
);
|
||||
const engine = await this.#engine(request.operation, signal);
|
||||
const engine = await this.#engineForObservation(
|
||||
request,
|
||||
payload,
|
||||
decompiled,
|
||||
signal,
|
||||
);
|
||||
const result = jebResultSchemas.decompile_jeb_item.parse({
|
||||
engine,
|
||||
unit_path: unit_path ?? null,
|
||||
@@ -406,7 +458,11 @@ export class JebProvider implements JebAnalysisPort {
|
||||
);
|
||||
}
|
||||
|
||||
#requireSuccess(payload: unknown, operation: JebOperation): unknown {
|
||||
#requireSuccess(
|
||||
payload: unknown,
|
||||
operation: JebOperation,
|
||||
targetPath?: string,
|
||||
): unknown {
|
||||
if (typeof payload !== "object" || payload === null)
|
||||
throw new AnalysisProtocolError(
|
||||
"JEB MCP returned a non-object envelope; no observation can be established",
|
||||
@@ -415,7 +471,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
if (success === false)
|
||||
throw new AnalysisUnsupportedTargetError(
|
||||
operation,
|
||||
this.#endpoint.toString(),
|
||||
targetPath ?? this.#endpoint.toString(),
|
||||
refusalReason(payload, "The engine refused the request"),
|
||||
);
|
||||
if (success !== true)
|
||||
@@ -475,7 +531,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
physical instanceof Error ? physical.message : String(physical),
|
||||
{
|
||||
cause,
|
||||
userMessage: `Start a JEB client serving MCP (default ${DEFAULT_JEB_MCP_ENDPOINT}) or set REA_JEB_MCP_URL to the running endpoint.`,
|
||||
userMessage: JEB_CONNECTION_REMEDIATION,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user