mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
fix: retain completed observations when later analysis steps fail
This commit is contained in:
@@ -118,6 +118,12 @@ selected overload. Native/abstract methods with no decompiled body report
|
||||
responses remain in Evidence. Source revision is populated only for bytes
|
||||
matching the audited release JAR.
|
||||
|
||||
If a later step fails, the error's `details.partial_observation` retains the
|
||||
selected and resolved APK paths, digest, observed provider identity and versions,
|
||||
and each upstream request and reply collected before the failure. Failed replies
|
||||
remain available even when their payload cannot be interpreted. Cancellation
|
||||
also stops target hashing before provider admission.
|
||||
|
||||
These operations do not execute the APK or provide split APK/AAB handling,
|
||||
signature validation, full resource-table semantics, native-library analysis or
|
||||
Android runtime capture. Existing artifact inventory/extraction tools can supply
|
||||
|
||||
+21
-17
@@ -123,24 +123,25 @@ const cleanup = async (
|
||||
resources.session = undefined;
|
||||
} catch (cause) {
|
||||
// Retain an uncertain workspace until the caller can resolve process ownership.
|
||||
return err(
|
||||
new ProviderCleanupError(
|
||||
"jadx",
|
||||
[
|
||||
session?.transport.runId ?? "unknown",
|
||||
...(root === undefined ? [] : [root.path]),
|
||||
],
|
||||
{
|
||||
...diagnostics,
|
||||
reason: cause instanceof Error ? cause.message : String(cause),
|
||||
provider_cleanup:
|
||||
cause instanceof ProviderCleanupError
|
||||
? (cause.diagnostics ?? null)
|
||||
: null,
|
||||
},
|
||||
{ cause: previousError ?? cause },
|
||||
),
|
||||
const error = new ProviderCleanupError(
|
||||
"jadx",
|
||||
[
|
||||
session?.transport.runId ?? "unknown",
|
||||
...(root === undefined ? [] : [root.path]),
|
||||
],
|
||||
{
|
||||
...diagnostics,
|
||||
reason: cause instanceof Error ? cause.message : String(cause),
|
||||
provider_cleanup:
|
||||
cause instanceof ProviderCleanupError
|
||||
? (cause.diagnostics ?? null)
|
||||
: null,
|
||||
},
|
||||
{ cause: previousError ?? cause },
|
||||
);
|
||||
if (previousError?.partialObservation !== undefined)
|
||||
error.retainPartialObservation(previousError.partialObservation);
|
||||
return err(error);
|
||||
}
|
||||
try {
|
||||
await root?.close();
|
||||
@@ -396,6 +397,9 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
signal: options?.signal,
|
||||
session,
|
||||
});
|
||||
const partialObservation = session?.partialObservation();
|
||||
if (partialObservation !== undefined)
|
||||
previousError.retainPartialObservation(partialObservation);
|
||||
// Admission can fail before selecting an existing session for execution.
|
||||
root ??= this.#retained?.root;
|
||||
session ??= this.#retained?.session;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type AnalysisExecution,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type { AndroidRequest } from "../domain/android/androidAnalysis.js";
|
||||
import type { AndroidPartialObservation } from "../domain/android/androidPartialObservation.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
@@ -35,6 +36,23 @@ export class JadxSession {
|
||||
readonly #client = new Client({ name: "rea-android-adapter", version: "1" });
|
||||
#loaded: ReturnType<typeof jadxLoadSchema.parse> | undefined;
|
||||
#connected = false;
|
||||
#partialContext:
|
||||
| {
|
||||
readonly operation: AndroidRequest["operation"];
|
||||
readonly selectedPath: string;
|
||||
readonly target: BinaryTarget;
|
||||
readonly jarHash: string;
|
||||
readonly bridgeHash: string;
|
||||
}
|
||||
| undefined;
|
||||
#observedCalls: {
|
||||
readonly operation: string;
|
||||
readonly input: Readonly<Record<string, JsonValue>>;
|
||||
readonly response: JsonValue;
|
||||
}[] = [];
|
||||
#serverName: string | null = null;
|
||||
#serverVersion: string | null = null;
|
||||
#engineVersion: string | null = null;
|
||||
|
||||
constructor(
|
||||
options: Omit<OwnedProviderProcessSpawnOptions, "runId" | "stdin">,
|
||||
@@ -53,6 +71,17 @@ export class JadxSession {
|
||||
readonly signal: AbortSignal;
|
||||
}): Promise<AnalysisExecution> {
|
||||
const { request, target, snapshot, jarHash, signal } = context;
|
||||
this.#partialContext = {
|
||||
operation: request.operation,
|
||||
selectedPath: request.input.path,
|
||||
target,
|
||||
jarHash,
|
||||
bridgeHash: context.bridgeHash,
|
||||
};
|
||||
this.#observedCalls = [];
|
||||
this.#serverName = null;
|
||||
this.#serverVersion = null;
|
||||
this.#engineVersion = null;
|
||||
const abort = () => {
|
||||
void this.transport.close().catch(() => undefined);
|
||||
};
|
||||
@@ -60,12 +89,13 @@ export class JadxSession {
|
||||
try {
|
||||
signal.throwIfAborted();
|
||||
this.transport.beginOperation();
|
||||
const raw: JsonValue[] = [];
|
||||
if (!this.#connected) {
|
||||
await this.#client.connect(this.transport, { timeout: 30_000 });
|
||||
this.#connected = true;
|
||||
}
|
||||
const server = this.#client.getServerVersion();
|
||||
this.#serverName = server?.name ?? null;
|
||||
this.#serverVersion = server?.version ?? null;
|
||||
if (
|
||||
server?.name !== JADX_BRIDGE_IDENTITY.name ||
|
||||
server.version !== JADX_BRIDGE_IDENTITY.version
|
||||
@@ -75,10 +105,11 @@ export class JadxSession {
|
||||
request.operation,
|
||||
`Expected ${JADX_BRIDGE_IDENTITY.name} ${JADX_BRIDGE_IDENTITY.version}; server reported ${server?.name ?? "unknown"} ${server?.version ?? "unknown"}.`,
|
||||
);
|
||||
const tools = this.#tools(request, target, signal, raw);
|
||||
const tools = this.#tools(request, target, signal);
|
||||
const runtime = jadxRuntimeSchema.parse(
|
||||
await tools.json("rea_jvm_status", {}),
|
||||
);
|
||||
this.#engineVersion = runtime.engine_reported_version;
|
||||
if (runtime.engine_reported_version !== JADX_RELEASE.version)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
@@ -117,7 +148,7 @@ export class JadxSession {
|
||||
bridge_sha256: context.bridgeHash,
|
||||
jar_sha256: jarHash,
|
||||
loaded,
|
||||
calls: raw,
|
||||
calls: this.#observedCalls,
|
||||
},
|
||||
subject: target,
|
||||
limitations: JADX_LIMITATIONS,
|
||||
@@ -131,6 +162,32 @@ export class JadxSession {
|
||||
}
|
||||
}
|
||||
|
||||
/** Facts and upstream replies retained if a later request step fails. */
|
||||
partialObservation(): AndroidPartialObservation | undefined {
|
||||
const context = this.#partialContext;
|
||||
if (context === undefined) return undefined;
|
||||
return {
|
||||
provider_id: "jadx",
|
||||
operation: context.operation,
|
||||
target: {
|
||||
selected_path: context.selectedPath,
|
||||
path: context.target.path,
|
||||
sha256: context.target.sha256,
|
||||
format: "apk",
|
||||
},
|
||||
provider_facts: {
|
||||
jar_sha256: context.jarHash,
|
||||
bridge_sha256: context.bridgeHash,
|
||||
bridge_name: JADX_BRIDGE_IDENTITY.name,
|
||||
bridge_version: JADX_BRIDGE_IDENTITY.version,
|
||||
server_name: this.#serverName,
|
||||
server_version: this.#serverVersion,
|
||||
engine_version: this.#engineVersion,
|
||||
},
|
||||
calls: this.#observedCalls,
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether the selected Java runtime established the REA bridge protocol. */
|
||||
initialized(): boolean {
|
||||
return this.#connected;
|
||||
@@ -149,7 +206,6 @@ export class JadxSession {
|
||||
request: AndroidRequest,
|
||||
target: BinaryTarget,
|
||||
signal: AbortSignal,
|
||||
raw: JsonValue[],
|
||||
): JadxToolPort {
|
||||
const call = async (
|
||||
name: string,
|
||||
@@ -160,12 +216,10 @@ export class JadxSession {
|
||||
{ name, arguments: input },
|
||||
{ timeout: 120_000, signal },
|
||||
);
|
||||
const observedResponse = jsonValueSchema.parse(response);
|
||||
const call = { operation: name, input, response: observedResponse };
|
||||
this.#observedCalls.push(call);
|
||||
const envelope = parseJadxEnvelope(response, request.operation);
|
||||
raw.push({
|
||||
operation: name,
|
||||
input,
|
||||
response: jsonValueSchema.parse(response),
|
||||
});
|
||||
if (envelope.failed) {
|
||||
if (envelope.text.startsWith("{")) {
|
||||
const failure = jadxInputFailureSchema.safeParse(
|
||||
|
||||
@@ -269,7 +269,8 @@ const runAnalysis = async (
|
||||
});
|
||||
if (evidence !== undefined) {
|
||||
const recorded = session.recordEvidence(evidence);
|
||||
if (!recorded.ok) return cliError(recorded.error);
|
||||
if (!recorded.ok)
|
||||
return cliError(recorded.error.retainPartialObservation(evidence));
|
||||
if (isWorkflowEvidenceTool(tool)) {
|
||||
const unknowns = recordWorkflowUnknowns({
|
||||
name: tool,
|
||||
@@ -277,7 +278,8 @@ const runAnalysis = async (
|
||||
evidenceId: evidence.evidence_id,
|
||||
recordUnknown: (unknown) => session.recordUnknown(unknown),
|
||||
});
|
||||
if (!unknowns.ok) return cliError(unknowns.error);
|
||||
if (!unknowns.ok)
|
||||
return cliError(unknowns.error.retainPartialObservation(evidence));
|
||||
}
|
||||
}
|
||||
if (
|
||||
@@ -290,7 +292,8 @@ const runAnalysis = async (
|
||||
const workflowRecord = workflowSnapshotRecord(evidence, tool);
|
||||
if (workflowRecord !== undefined) {
|
||||
const recorded = session.recordWorkflowSnapshot(workflowRecord);
|
||||
if (!recorded.ok) return cliError(recorded.error);
|
||||
if (!recorded.ok)
|
||||
return cliError(recorded.error.retainPartialObservation(evidence));
|
||||
}
|
||||
}
|
||||
if (
|
||||
@@ -299,13 +302,15 @@ const runAnalysis = async (
|
||||
evidence !== undefined
|
||||
) {
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
if (!snapshot.ok) return cliError(snapshot.error);
|
||||
if (!snapshot.ok)
|
||||
return cliError(snapshot.error.retainPartialObservation(evidence));
|
||||
const written = await writeAnalysisSnapshot(
|
||||
snapshot.value,
|
||||
snapshotPath,
|
||||
true,
|
||||
);
|
||||
if (!written.ok) return cliError(written.error);
|
||||
if (!written.ok)
|
||||
return cliError(written.error.retainPartialObservation(evidence));
|
||||
}
|
||||
return output;
|
||||
},
|
||||
|
||||
@@ -77,8 +77,14 @@ export abstract class AnalysisError extends Error {
|
||||
readonly userMessage: string | undefined = undefined;
|
||||
readonly userCategory: "cancelled" | undefined = undefined;
|
||||
readonly executionFailure: string | undefined = undefined;
|
||||
readonly partialObservation: AnalysisPartialObservation | undefined;
|
||||
partialObservation: AnalysisPartialObservation | undefined;
|
||||
readonly cleanupReport: ProcessCaptureCleanupReport | undefined = undefined;
|
||||
|
||||
/** Retain a completed observation when a later step fails. */
|
||||
retainPartialObservation(observation: AnalysisPartialObservation): this {
|
||||
this.partialObservation ??= observation;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
import type {
|
||||
PartialProcessCaptureObservation,
|
||||
@@ -90,6 +96,8 @@ import type { ElectronActivePartialObservation } from "./javascript/electronActi
|
||||
import type { JavaScriptRuntimeObservation } from "./javascript/javascriptRuntimeObservation.js";
|
||||
import type { ArtifactInventoryPartialObservation } from "./artifactPartialObservation.js";
|
||||
import type { BrowserScenarioPartialObservation } from "./browserScenarioCapture.js";
|
||||
import type { Evidence } from "./evidence.js";
|
||||
import type { AndroidPartialObservation } from "./android/androidPartialObservation.js";
|
||||
|
||||
/** Provider-neutral evidence collected before a typed analysis failure. */
|
||||
export type AnalysisPartialObservation =
|
||||
@@ -99,4 +107,6 @@ export type AnalysisPartialObservation =
|
||||
| ElectronActivePartialObservation
|
||||
| BrowserScenarioPartialObservation
|
||||
| JavaScriptRuntimeObservation
|
||||
| ArtifactInventoryPartialObservation;
|
||||
| ArtifactInventoryPartialObservation
|
||||
| Evidence
|
||||
| AndroidPartialObservation;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
|
||||
const operationSchema = z.enum([
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"inspect_android_class",
|
||||
"inspect_android_method",
|
||||
"trace_android_references",
|
||||
]);
|
||||
|
||||
/** JSON-safe Android provider facts collected before an analysis failure. */
|
||||
export const androidPartialObservationSchema = z.strictObject({
|
||||
provider_id: z.literal("jadx"),
|
||||
operation: operationSchema,
|
||||
target: z.strictObject({
|
||||
selected_path: localPathStringSchema,
|
||||
path: localPathStringSchema,
|
||||
sha256: digestSchema,
|
||||
format: z.literal("apk"),
|
||||
}),
|
||||
provider_facts: z.strictObject({
|
||||
jar_sha256: digestSchema.nullable(),
|
||||
bridge_sha256: digestSchema.nullable(),
|
||||
bridge_name: z.string().min(1),
|
||||
bridge_version: z.string().min(1),
|
||||
server_name: z.string().nullable(),
|
||||
server_version: z.string().nullable(),
|
||||
engine_version: z.string().min(1).nullable(),
|
||||
}),
|
||||
calls: z.array(
|
||||
z.strictObject({
|
||||
operation: z.string().min(1),
|
||||
input: jsonObjectSchema,
|
||||
response: jsonValueSchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
/** JSON-safe Android provider facts retained when analysis fails. */
|
||||
export type AndroidPartialObservation = z.infer<
|
||||
typeof androidPartialObservationSchema
|
||||
>;
|
||||
@@ -5,7 +5,9 @@ import { expect, it as test, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { access } from "node:fs/promises";
|
||||
import { ANDROID_TOOL_CONTRACTS } from "../../../src/contracts/android/androidToolContracts.js";
|
||||
import { androidPartialObservationSchema } from "../../../src/domain/android/androidPartialObservation.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { ToolResultDelivery } from "../../../src/server/toolResult.js";
|
||||
import { startMcpTransport } from "../../../src/main/transport.js";
|
||||
@@ -14,6 +16,7 @@ import type { AndroidAnalysisPort } from "../../../src/application/android/Andro
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import {
|
||||
createJadxProtocolFixture,
|
||||
verifyJadxFixtureCleanup,
|
||||
@@ -148,6 +151,137 @@ it("publishes and executes all APK contracts with inline Evidence and no active
|
||||
await verifyJadxFixtureCleanup(launches);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
scenario: "failed upstream reply",
|
||||
mode: "tool-error",
|
||||
errorCode: "execution_failure",
|
||||
serverName: "rea-jadx-bridge",
|
||||
serverVersion: "1",
|
||||
engineVersion: "0.7.1",
|
||||
calls: [
|
||||
{ operation: "rea_jvm_status" },
|
||||
{ operation: "load_apk" },
|
||||
{ operation: "get_app_info" },
|
||||
{
|
||||
operation: "get_android_manifest",
|
||||
response: { isError: true },
|
||||
},
|
||||
],
|
||||
containsAmbientEnvironment: false,
|
||||
},
|
||||
{
|
||||
scenario: "transport error after successful replies",
|
||||
mode: "rpc-disconnect",
|
||||
errorCode: "execution_failure",
|
||||
serverName: "rea-jadx-bridge",
|
||||
serverVersion: "1",
|
||||
engineVersion: "0.7.1",
|
||||
calls: [{ operation: "rea_jvm_status" }, { operation: "load_apk" }],
|
||||
containsAmbientEnvironment: true,
|
||||
},
|
||||
{
|
||||
scenario: "empty handshake identity mismatch",
|
||||
mode: "empty-server-identity",
|
||||
errorCode: "capability_unavailable",
|
||||
serverName: "",
|
||||
serverVersion: "",
|
||||
engineVersion: null,
|
||||
calls: [],
|
||||
containsAmbientEnvironment: true,
|
||||
},
|
||||
])(
|
||||
"retains Android partial observations for $scenario through MCP",
|
||||
async ({
|
||||
mode,
|
||||
errorCode,
|
||||
serverName,
|
||||
serverVersion,
|
||||
engineVersion,
|
||||
calls,
|
||||
containsAmbientEnvironment,
|
||||
}) => {
|
||||
const { provider, apk, launches } = await createJadxProtocolFixture(mode);
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("Unexpected native provider acquisition");
|
||||
});
|
||||
const server = createServer(
|
||||
{ kind: "session", session },
|
||||
{ androidAnalysis: provider },
|
||||
);
|
||||
const client = new Client({
|
||||
name: "android-partial-observation-regression",
|
||||
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 response = await client.callTool({
|
||||
name: "inspect_android_package",
|
||||
arguments: { path: apk },
|
||||
});
|
||||
const projected = parseMcpToolError(response).error;
|
||||
expect(projected.code).toBe(errorCode);
|
||||
const details = z.record(z.string(), z.unknown()).parse(projected.details);
|
||||
const partial = androidPartialObservationSchema.parse(
|
||||
details.partial_observation,
|
||||
);
|
||||
expect(partial).toMatchObject({
|
||||
provider_id: "jadx",
|
||||
operation: "inspect_android_package",
|
||||
target: { selected_path: apk, path: apk, format: "apk" },
|
||||
provider_facts: {
|
||||
bridge_name: "rea-jadx-bridge",
|
||||
bridge_version: "1",
|
||||
server_name: serverName,
|
||||
server_version: serverVersion,
|
||||
engine_version: engineVersion,
|
||||
},
|
||||
calls,
|
||||
});
|
||||
if (!containsAmbientEnvironment)
|
||||
expect(JSON.stringify(partial)).not.toContain("JAVA_HOME");
|
||||
await client.close();
|
||||
await server.close();
|
||||
await verifyJadxFixtureCleanup(launches);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains partial JADX replies when owned cleanup also fails", async () => {
|
||||
const { service, apk } = await createJadxProtocolFixture("cleanup-failure");
|
||||
const result = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
});
|
||||
if (result.ok) throw new Error("Expected the fixture tool to fail");
|
||||
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.code).toBe("cleanup_incomplete");
|
||||
expect(projected.details).toMatchObject({
|
||||
cleanup: "incomplete",
|
||||
execution_failure: "execution_failure",
|
||||
partial_observation: {
|
||||
provider_id: "jadx",
|
||||
operation: "inspect_android_package",
|
||||
calls: [
|
||||
{ operation: "rea_jvm_status" },
|
||||
{ operation: "load_apk" },
|
||||
{ operation: "get_app_info" },
|
||||
{
|
||||
operation: "get_android_manifest",
|
||||
response: { isError: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("cleans an active engine when the MCP client disconnects", async () => {
|
||||
const { provider, apk, launches } = await createJadxProtocolFixture("stall");
|
||||
const session = createTestBinarySession(() => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { runDirectAnalysis } from "../../src/application/DirectAnalysis.js";
|
||||
import type {
|
||||
@@ -18,7 +19,11 @@ import {
|
||||
readAnalysisSnapshot,
|
||||
writeAnalysisSnapshot,
|
||||
} from "../../src/application/binary/AnalysisSnapshotFiles.js";
|
||||
import { createEvidence, type Evidence } from "../../src/domain/evidence.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
type Evidence,
|
||||
} from "../../src/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js";
|
||||
import {
|
||||
REA_WORKFLOW_PROVIDER,
|
||||
@@ -646,7 +651,51 @@ describe("snapshot replay provider identity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const assertEvidenceSurvivesSnapshotSaveFailure = async (): Promise<void> => {
|
||||
const directory = await createTestTempDirectory("rea-workflow-save-error-");
|
||||
const path = join(directory, "fixture.hop");
|
||||
const snapshotPath = join(directory, "missing", "snapshot.json");
|
||||
await writeFile(path, "fixture");
|
||||
const starts: string[] = [];
|
||||
const calls: string[] = [];
|
||||
const provider = makeProvider(starts, calls);
|
||||
const dependencies: DirectAnalysisDependencies = {
|
||||
readConfiguration: () => parseConfig({}),
|
||||
createBinarySession: () => createTestBinarySession(provider),
|
||||
createManagedBinarySession: () => createTestBinarySession(provider),
|
||||
};
|
||||
|
||||
const result = await runDirectAnalysis(
|
||||
dependencies,
|
||||
path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{ snapshotPath },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
partial_observation: {
|
||||
operation: "binary_overview",
|
||||
subject: { local_path: path },
|
||||
},
|
||||
},
|
||||
});
|
||||
const partial = z
|
||||
.object({
|
||||
details: z.object({ partial_observation: z.unknown() }),
|
||||
})
|
||||
.parse(result).details.partial_observation;
|
||||
expect(parseEvidence(partial).operation).toBe("binary_overview");
|
||||
expect(calls.toSorted()).toEqual(operations.toSorted());
|
||||
};
|
||||
|
||||
describe("workflow snapshot profile and cancellation binding", () => {
|
||||
it(
|
||||
"preserves completed Evidence when CLI snapshot persistence fails",
|
||||
assertEvidenceSurvivesSnapshotSaveFailure,
|
||||
);
|
||||
it("does not replay a valid entry from a different workflow profile", async () => {
|
||||
const directory = await createTestTempDirectory("rea-workflow-profile-");
|
||||
const path = join(directory, "fixture.hop");
|
||||
|
||||
Vendored
+8
-6
@@ -6,8 +6,9 @@ import { z } from "zod";
|
||||
// It is a protocol fixture, not evidence of JADX decompiler support.
|
||||
const mode = process.env.REA_FAKE_JADX_MODE ?? "normal";
|
||||
const server = new McpServer({
|
||||
name: "rea-jadx-bridge",
|
||||
version: mode === "version" ? "99.0.0" : "1",
|
||||
name: mode === "empty-server-identity" ? "" : "rea-jadx-bridge",
|
||||
version:
|
||||
mode === "empty-server-identity" ? "" : mode === "version" ? "99.0.0" : "1",
|
||||
});
|
||||
const classes = [
|
||||
"fixture.Target",
|
||||
@@ -60,16 +61,17 @@ register("load_apk", async ({ path }) => {
|
||||
resources: "full",
|
||||
});
|
||||
});
|
||||
register("get_app_info", () =>
|
||||
json({
|
||||
register("get_app_info", () => {
|
||||
if (mode === "rpc-disconnect") process.exit(17);
|
||||
return json({
|
||||
package: "fixture",
|
||||
version_name: "1",
|
||||
version_code: "1",
|
||||
min_sdk: "26",
|
||||
target_sdk: "33",
|
||||
permissions: ["android.permission.INTERNET"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
register("get_android_manifest", () => {
|
||||
if (mode === "tool-error" || mode === "cleanup-failure")
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user