mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(decompile): retain limitations across failed resume (#7913)
This commit is contained in:
1 parent
31c028f496
commit
5082b50f07
2 files changed
+122
-19
No files matched your search
@@ -426,13 +426,12 @@ const attemptOne = async (
|
||||
"The function file already exists and does not match this export.",
|
||||
};
|
||||
recordFailure(input.state, procedure, reason);
|
||||
const retained = retainedObservation(input.session, procedure.address);
|
||||
const appended = await input.session.append(
|
||||
failureRecord(
|
||||
procedure,
|
||||
retainedBody(input.session, procedure.address),
|
||||
reason,
|
||||
{ relativePath: input.session.pathFor(procedure.address) },
|
||||
),
|
||||
failureRecord(procedure, retained.body, reason, {
|
||||
relativePath: input.session.pathFor(procedure.address),
|
||||
...retained.options,
|
||||
}),
|
||||
);
|
||||
return { result: appended, bodySupport };
|
||||
}
|
||||
@@ -442,13 +441,12 @@ const attemptOne = async (
|
||||
message: "The existing function file could not be read.",
|
||||
};
|
||||
recordFailure(input.state, procedure, reason);
|
||||
const retained = retainedObservation(input.session, procedure.address);
|
||||
const appended = await input.session.append(
|
||||
failureRecord(
|
||||
procedure,
|
||||
retainedBody(input.session, procedure.address),
|
||||
reason,
|
||||
{ relativePath: input.session.pathFor(procedure.address) },
|
||||
),
|
||||
failureRecord(procedure, retained.body, reason, {
|
||||
relativePath: input.session.pathFor(procedure.address),
|
||||
...retained.options,
|
||||
}),
|
||||
);
|
||||
return { result: appended, bodySupport };
|
||||
}
|
||||
@@ -941,12 +939,19 @@ const unavailableBody = (reason: string): FunctionBody => ({
|
||||
reason: reason.length === 0 ? "unavailable" : reason,
|
||||
});
|
||||
|
||||
/** Keep the journal's body fact when a conflicting file is left unchanged. */
|
||||
const retainedBody = (
|
||||
/** Preserve the observations and their caveats when a file cannot be reused. */
|
||||
const retainedObservation = (
|
||||
session: DecompileExport,
|
||||
address: string,
|
||||
): FunctionBody => {
|
||||
return session.record(address)?.body ?? unavailableBody("not rewritten");
|
||||
): { body: FunctionBody; options: { limitations?: readonly string[] } } => {
|
||||
const record = session.record(address);
|
||||
return {
|
||||
body: record?.body ?? unavailableBody("not rewritten"),
|
||||
options:
|
||||
record?.limitations === undefined
|
||||
? {}
|
||||
: { limitations: record.limitations },
|
||||
};
|
||||
};
|
||||
|
||||
const exportLimitations = (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type AnalysisOperationPort,
|
||||
} from "../../../src/application/AnalysisProvider.js";
|
||||
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
||||
import { DecompileExport } from "../../../src/application/decompileDirectory/exportTree.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { err, ok } from "../../../src/domain/result.js";
|
||||
import {
|
||||
@@ -56,9 +57,106 @@ const addressCaveats: AnalysisOperationPort = {
|
||||
},
|
||||
};
|
||||
|
||||
afterEach(closeEnhancedToolResources);
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await closeEnhancedToolResources();
|
||||
});
|
||||
|
||||
describe("decompile_all MCP caveats", () => {
|
||||
it("retains all observed caveats across unreadable and conflicting resumes", async () => {
|
||||
const parent = await createTestTempDirectory(
|
||||
"rea-decompile-resume-caveats-",
|
||||
);
|
||||
const output = join(parent, "out");
|
||||
const selected = join(output, "functions", "0x1000.c");
|
||||
const caveats = Array.from(
|
||||
{ length: 40 },
|
||||
(_, i) => `provider limitation ${i}`,
|
||||
);
|
||||
const calls: string[] = [];
|
||||
const port: AnalysisOperationPort = {
|
||||
execute: async (operation) => {
|
||||
calls.push(operation);
|
||||
return ok(
|
||||
createAnalysisExecution(
|
||||
operation === "list_procedures"
|
||||
? [{ address: "0x1000", value: "function" }]
|
||||
: operation === "procedure_info"
|
||||
? null
|
||||
: "int f(void) { return 0; }",
|
||||
provider,
|
||||
{
|
||||
analysisProfile: profile,
|
||||
...(operation === "list_procedures"
|
||||
? {
|
||||
subject: {
|
||||
path: "/tmp/test",
|
||||
sha256: "ab".repeat(32),
|
||||
format: "elf" as const,
|
||||
architecture: "x86_64" as const,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(operation === "procedure_pseudo_code"
|
||||
? { limitations: caveats }
|
||||
: {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
const client = await connect(port);
|
||||
const run = async (resume: boolean) =>
|
||||
client.callTool({
|
||||
name: "decompile_all",
|
||||
arguments: { output_directory: output, resume },
|
||||
});
|
||||
const record = async () =>
|
||||
decompileDirectoryIndexSchema.parse(
|
||||
JSON.parse(
|
||||
await readFile(join(output, "decompile-index.json"), "utf8"),
|
||||
),
|
||||
).functions[0];
|
||||
expect((await run(false)).isError).not.toBe(true);
|
||||
const original = await readFile(selected, "utf8");
|
||||
const hashFile = DecompileExport.prototype.hashFile;
|
||||
const blocked = vi
|
||||
.spyOn(DecompileExport.prototype, "hashFile")
|
||||
.mockImplementation(function (this: DecompileExport, path, signal) {
|
||||
return path === "functions/0x1000.c"
|
||||
? Promise.reject(
|
||||
Object.assign(new Error("access denied"), { code: "EACCES" }),
|
||||
)
|
||||
: hashFile.call(this, path, signal);
|
||||
});
|
||||
expect((await run(true)).isError).not.toBe(true);
|
||||
expect(await record()).toMatchObject({
|
||||
status: "failed",
|
||||
failure: { code: "output_io" },
|
||||
limitations: caveats,
|
||||
relative_path: "functions/0x1000.c",
|
||||
});
|
||||
blocked.mockRestore();
|
||||
await writeFile(selected, "external change\n");
|
||||
for (let i = 0; i < 2; i++) {
|
||||
expect((await run(true)).isError).not.toBe(true);
|
||||
expect(await record()).toMatchObject({
|
||||
status: "failed",
|
||||
failure: { code: "output_exists" },
|
||||
limitations: caveats,
|
||||
relative_path: "functions/0x1000.c",
|
||||
});
|
||||
expect(await readFile(selected, "utf8")).toBe("external change\n");
|
||||
}
|
||||
await writeFile(selected, original);
|
||||
calls.length = 0;
|
||||
expect((await run(true)).isError).not.toBe(true);
|
||||
expect(await record()).toMatchObject({
|
||||
status: "written",
|
||||
limitations: caveats,
|
||||
});
|
||||
expect(calls).not.toContain("procedure_pseudo_code");
|
||||
});
|
||||
it("returns a bounded caveat summary and keeps each function's caveats in its file", async () => {
|
||||
const parent = await createTestTempDirectory("rea-decompile-caveats-");
|
||||
const client = await connect(addressCaveats);
|
||||
|
||||
Reference in new issue
Block a user