mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(decompile): name the differing resume identity (#8465)
Resume refused every identity mismatch with one generic message and generic input-schema recovery. Report the differing target, profile, layout, or provider facts with the journal and current values so callers can choose the matching provider/profile or a new output directory. Fixes #8349
This commit is contained in:
1 parent
71746351d3
commit
1aa6beb5dc
2 files changed
+100
-24
No files matched your search
@@ -138,6 +138,7 @@ const harness = (options?: {
|
||||
Record<string, "error" | "timeout" | "empty" | "null">
|
||||
>;
|
||||
readonly info?: "ok" | "unavailable" | "error";
|
||||
readonly listProfile?: typeof profile;
|
||||
readonly subject?: boolean;
|
||||
readonly mismatch?: string;
|
||||
readonly unprofiled?: string;
|
||||
@@ -170,7 +171,7 @@ const harness = (options?: {
|
||||
})),
|
||||
provider,
|
||||
{
|
||||
analysisProfile: profile,
|
||||
analysisProfile: options?.listProfile ?? profile,
|
||||
...(options?.subject === false ? {} : { subject }),
|
||||
},
|
||||
),
|
||||
@@ -614,6 +615,37 @@ describe("decompile_all resume", () => {
|
||||
await readFile(join(directory(), "functions/0x1000.c"), "utf8"),
|
||||
).toContain(sentinel);
|
||||
});
|
||||
|
||||
it("names the resume identity that differs instead of directing callers to the input schema", async () => {
|
||||
expect((await run(harness().tools, directory())).ok).toBe(true);
|
||||
|
||||
const resumed = await run(
|
||||
harness({ listProfile: otherProfile }).tools,
|
||||
directory(),
|
||||
{ resume: true },
|
||||
);
|
||||
|
||||
expect(resumed.ok).toBe(false);
|
||||
if (resumed.ok) return;
|
||||
expect(resumed.error).toBeInstanceOf(AnalysisInputError);
|
||||
if (!(resumed.error instanceof AnalysisInputError)) return;
|
||||
expect(resumed.error.userMessage).toBe(
|
||||
"The decompile journal does not match the current analysis profile digest.",
|
||||
);
|
||||
expect(resumed.error.conformsToInputContract).toBe(true);
|
||||
expect(resumed.error.remediationAction).toContain("new output directory");
|
||||
expect(resumed.error.issues).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["output_directory"],
|
||||
expected: expect.objectContaining({
|
||||
analysis_profile_digest: profile.digest,
|
||||
}),
|
||||
observed: expect.objectContaining({
|
||||
analysis_profile_digest: otherProfile.digest,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decompile_all filters and layout", () => {
|
||||
|
||||
@@ -29,9 +29,9 @@ import {
|
||||
type DecompileJournalHeader,
|
||||
} from "../../contracts/decompileDirectory.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
type AnalysisInputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
@@ -663,20 +663,18 @@ const readJournal = async (
|
||||
if (!parsed.ok) return parsed;
|
||||
const unknown = unknownIdentity(parsed.value.header, identity);
|
||||
if (unknown !== undefined) return err(unknown);
|
||||
return sameIdentity(parsed.value.header, identity)
|
||||
? ok({
|
||||
records: parsed.value.records,
|
||||
written: parsed.value.written,
|
||||
paths: parsed.value.paths,
|
||||
committedBytes: parsed.value.committedBytes,
|
||||
journalBytes: parsed.value.journalBytes,
|
||||
})
|
||||
: err(
|
||||
decompileInputError(
|
||||
"output_directory",
|
||||
"The decompile journal does not match this target, provider, analysis profile, or layout.",
|
||||
),
|
||||
);
|
||||
const mismatches = identityMismatches(parsed.value.header, identity);
|
||||
if (mismatches.length > 0)
|
||||
return err(
|
||||
journalIdentityMismatch(parsed.value.header, identity, mismatches),
|
||||
);
|
||||
return ok({
|
||||
records: parsed.value.records,
|
||||
written: parsed.value.written,
|
||||
paths: parsed.value.paths,
|
||||
committedBytes: parsed.value.committedBytes,
|
||||
journalBytes: parsed.value.journalBytes,
|
||||
});
|
||||
};
|
||||
|
||||
const parseJournal = (
|
||||
@@ -747,16 +745,62 @@ const unknownIdentity = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const sameIdentity = (
|
||||
/** Identity components in the order presented to the caller. */
|
||||
const identityMismatches = (
|
||||
header: DecompileJournalHeader,
|
||||
identity: ExportIdentity,
|
||||
): boolean =>
|
||||
header.target_sha256 === identity.targetSha256 &&
|
||||
header.analysis_profile_digest === identity.analysisProfileDigest &&
|
||||
header.layout === identity.layout &&
|
||||
header.provider.id === identity.provider.id &&
|
||||
header.provider.name === identity.provider.name &&
|
||||
header.provider.version === identity.provider.version;
|
||||
): readonly string[] => [
|
||||
...(header.target_sha256 === identity.targetSha256 ? [] : ["target SHA-256"]),
|
||||
...(header.analysis_profile_digest === identity.analysisProfileDigest
|
||||
? []
|
||||
: ["analysis profile digest"]),
|
||||
...(header.layout === identity.layout ? [] : ["layout"]),
|
||||
...(header.provider.id === identity.provider.id ? [] : ["provider id"]),
|
||||
...(header.provider.name === identity.provider.name ? [] : ["provider name"]),
|
||||
...(header.provider.version === identity.provider.version
|
||||
? []
|
||||
: ["provider version"]),
|
||||
];
|
||||
|
||||
/**
|
||||
* Report the concrete resume identity that differs. The request is valid, so
|
||||
* generic input-schema recovery would send the caller to the wrong boundary.
|
||||
*/
|
||||
const journalIdentityMismatch = (
|
||||
header: DecompileJournalHeader,
|
||||
identity: ExportIdentity,
|
||||
mismatches: readonly string[],
|
||||
): AnalysisInputError => {
|
||||
const message = `The decompile journal does not match the current ${mismatches.join(", ")}.`;
|
||||
return new AnalysisInputError(
|
||||
"decompile_all",
|
||||
{
|
||||
userMessage: message,
|
||||
conformsToInputContract: true,
|
||||
remediationAction:
|
||||
"Resume with the target, provider, analysis profile, and layout that created this directory, or choose a new output directory. The journal and existing files were left unchanged.",
|
||||
},
|
||||
[
|
||||
{
|
||||
path: ["output_directory"],
|
||||
reason: "invalid_value",
|
||||
message,
|
||||
expected: {
|
||||
target_sha256: header.target_sha256,
|
||||
analysis_profile_digest: header.analysis_profile_digest,
|
||||
layout: header.layout,
|
||||
provider: header.provider,
|
||||
},
|
||||
observed: {
|
||||
target_sha256: identity.targetSha256,
|
||||
analysis_profile_digest: identity.analysisProfileDigest,
|
||||
layout: identity.layout,
|
||||
provider: identity.provider,
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const incompatibleJournal = (): AnalysisInputError =>
|
||||
decompileInputError(
|
||||
|
||||
Reference in new issue
Block a user