fix(tests): clear lint and typecheck errors on main
Split two over-long test functions into module-level helpers (max-lines-per-function): IdaSessionClient transport cases, IDA provider composition, and function comparison identity cases keep their names and assertions. Also fix three typecheck errors: default omitted direct_invokes_exhaustive to null so the result schema stays JsonValue compatible, parse the ghidra dossier before spreading it, and delete the optional beforeCall hook instead of assigning undefined under exactOptionalPropertyTypes.
This commit is contained in:
parent
7369b5ee63
commit
6e25cc3c76
|
|
@ -795,7 +795,7 @@ it("rejects unsupported parent operations and incompatible collections", () => {
|
|||
|
||||
it("preserves selected native facet unavailability instead of an exhausted empty page", () => {
|
||||
const parent = nativeParent({
|
||||
...ghidraFunctionDossier(),
|
||||
...functionDossierSchema.parse(ghidraFunctionDossier()),
|
||||
facet_availability: {
|
||||
comments: { available: false, reason: "Typed comments unavailable" },
|
||||
references: { available: false, reason: "Typed references unavailable" },
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export const androidResultSchemas = {
|
|||
source_line: z.null(),
|
||||
dex_offset: z.null(),
|
||||
/** false means transport-bounded instruction evidence; null/omitted is unknown. */
|
||||
direct_invokes_exhaustive: z.boolean().nullable().optional(),
|
||||
direct_invokes_exhaustive: z.boolean().nullable().default(null),
|
||||
/** Raw direct invoke instructions inside this using node, when available. */
|
||||
direct_invokes: z
|
||||
.array(
|
||||
|
|
|
|||
|
|
@ -128,6 +128,49 @@ const observe = (
|
|||
},
|
||||
);
|
||||
|
||||
const rejectsMalformedFacetAvailability = (): void => {
|
||||
expect(
|
||||
functionDossierSchema.safeParse({
|
||||
...dossier("return 0;", "0x1000"),
|
||||
facet_availability: { cfg: { available: false, reason: "" } },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
functionDossierSchema.safeParse({
|
||||
...dossier("return 0;", "0x1000"),
|
||||
facet_availability: { cfg: undefined },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
};
|
||||
|
||||
const keepsGeneratedNamesAmbiguous = (): void => {
|
||||
const named = (base: "0x1000" | "0x2000", name: string) =>
|
||||
functionDossierSchema.parse({
|
||||
...dossier("return 0;", base),
|
||||
procedure: {
|
||||
...procedure(base, name),
|
||||
signature: "int helper(void)",
|
||||
locals: [],
|
||||
},
|
||||
});
|
||||
const generated = compareFunctions(
|
||||
observe("1", named("0x1000", "sub_1000")),
|
||||
observe("2", named("0x2000", "sub_2000")),
|
||||
);
|
||||
expect(generated.function_match.status).toBe("ambiguous");
|
||||
expect(generated.status).toBe("unknown");
|
||||
expect(
|
||||
generated.dimensions.find(({ dimension }) => dimension === "identity"),
|
||||
).toMatchObject({ status: "unknown" });
|
||||
|
||||
const stable = compareFunctions(
|
||||
observe("3", named("0x1000", "firstFunction")),
|
||||
observe("4", named("0x2000", "secondFunction")),
|
||||
);
|
||||
expect(stable.function_match.status).toBe("mismatched");
|
||||
expect(stable.status).toBe("changed");
|
||||
};
|
||||
|
||||
describe("function comparison normalized identity", () => {
|
||||
it("retains unresolved call details alongside missing facet coverage", () => {
|
||||
const value: Record<string, unknown> = {
|
||||
|
|
@ -187,47 +230,14 @@ describe("function comparison normalized identity", () => {
|
|||
),
|
||||
).toMatchObject({ status: "unknown", left_count: null, right_count: null });
|
||||
});
|
||||
it("rejects malformed facet availability instead of treating it as absence", () => {
|
||||
expect(
|
||||
functionDossierSchema.safeParse({
|
||||
...dossier("return 0;", "0x1000"),
|
||||
facet_availability: { cfg: { available: false, reason: "" } },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
functionDossierSchema.safeParse({
|
||||
...dossier("return 0;", "0x1000"),
|
||||
facet_availability: { cfg: undefined },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
it("keeps differing generated names ambiguous and stable names mismatched", () => {
|
||||
const named = (base: "0x1000" | "0x2000", name: string) =>
|
||||
functionDossierSchema.parse({
|
||||
...dossier("return 0;", base),
|
||||
procedure: {
|
||||
...procedure(base, name),
|
||||
signature: "int helper(void)",
|
||||
locals: [],
|
||||
},
|
||||
});
|
||||
const generated = compareFunctions(
|
||||
observe("1", named("0x1000", "sub_1000")),
|
||||
observe("2", named("0x2000", "sub_2000")),
|
||||
);
|
||||
expect(generated.function_match.status).toBe("ambiguous");
|
||||
expect(generated.status).toBe("unknown");
|
||||
expect(
|
||||
generated.dimensions.find(({ dimension }) => dimension === "identity"),
|
||||
).toMatchObject({ status: "unknown" });
|
||||
|
||||
const stable = compareFunctions(
|
||||
observe("3", named("0x1000", "firstFunction")),
|
||||
observe("4", named("0x2000", "secondFunction")),
|
||||
);
|
||||
expect(stable.function_match.status).toBe("mismatched");
|
||||
expect(stable.status).toBe("changed");
|
||||
});
|
||||
it(
|
||||
"rejects malformed facet availability instead of treating it as absence",
|
||||
rejectsMalformedFacetAvailability,
|
||||
);
|
||||
it(
|
||||
"keeps differing generated names ambiguous and stable names mismatched",
|
||||
keepsGeneratedNamesAmbiguous,
|
||||
);
|
||||
|
||||
it.each(["sub_deallocate", "sub_deadbeef_handler", "fcn.dispatch"])(
|
||||
"recognizes the explicit symbol %s without a generated-name prefix false positive",
|
||||
|
|
|
|||
|
|
@ -1459,7 +1459,7 @@ it.each(["idb_close", "idb_list", "connection_close"])(
|
|||
});
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
} finally {
|
||||
producer.beforeCall = undefined;
|
||||
delete producer.beforeCall;
|
||||
producer.close = originalClose;
|
||||
await client.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,73 @@ afterEach(async () => {
|
|||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const reobservesExternallyMutableState = async (): Promise<void> => {
|
||||
const { root, target } = await createIdaTarget();
|
||||
roots.push(root);
|
||||
const path = join(root, "registration.json");
|
||||
await writeFile(path, JSON.stringify({ command: "fixture" }));
|
||||
const config = parseConfig({ REA_IDA_MCP_CONFIG: path });
|
||||
if (!config.ok) throw config.error;
|
||||
const producer = new RecordingIdaMcp(target);
|
||||
const session = createTestBinarySession(
|
||||
new IdaProvider(config.value, {}, () => producer),
|
||||
);
|
||||
expect((await session.open(target.path)).ok).toBe(true);
|
||||
const query = { procedure: "main", document: target.path };
|
||||
const first = await session.execute("procedure_pseudo_code", query);
|
||||
expect(first.ok && first.value.result).toContain("42");
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
if (!snapshot.ok) throw snapshot.error;
|
||||
expect(snapshot.value.entries).toEqual([]);
|
||||
const profile = session.analysisProfile();
|
||||
if (profile === undefined) throw new Error("Missing profile");
|
||||
const cached = createAnalysisExecution(
|
||||
"stale output",
|
||||
IDA_PROVIDER_IDENTITY,
|
||||
{
|
||||
analysisProfile: profile,
|
||||
subject: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
format: target.format,
|
||||
...(target.architecture === undefined
|
||||
? {}
|
||||
: { architecture: target.architecture }),
|
||||
},
|
||||
},
|
||||
);
|
||||
snapshot.value.entries.push(
|
||||
createAnalysisSnapshotEntry({
|
||||
target: snapshotTarget(target),
|
||||
binding: snapshotBinding(profile),
|
||||
operation: "procedure_pseudo_code",
|
||||
parameters: query,
|
||||
execution: cached,
|
||||
}),
|
||||
);
|
||||
snapshot.value.evidence_bundle = createEvidenceBundle([
|
||||
...snapshot.value.evidence_bundle.records,
|
||||
createEvidence(target, IDA_PROVIDER_IDENTITY, {
|
||||
operation: "procedure_pseudo_code",
|
||||
parameters: query,
|
||||
result: cached.result,
|
||||
rawResult: cached.rawResult,
|
||||
analysisProfile: profile,
|
||||
}),
|
||||
]);
|
||||
expect(session.importAnalysisSnapshot(snapshot.value).ok).toBe(true);
|
||||
expect(session.allowsSnapshotReplay("procedure_pseudo_code")).toBe(false);
|
||||
expect(session.allowsSnapshotReplay("binary_overview")).toBe(false);
|
||||
producer.pseudocode = "int main(void) { return 99; }";
|
||||
const second = await session.execute("procedure_pseudo_code", query);
|
||||
expect(second.ok && second.value.result).toContain("99");
|
||||
expect(
|
||||
producer.calls.filter(({ name }) => name === "decompile_function"),
|
||||
).toHaveLength(2);
|
||||
await session.close();
|
||||
expect(producer.closes).toBe(1);
|
||||
};
|
||||
|
||||
describe("IDA provider composition", () => {
|
||||
it("discovers without launching and commits adapter semantics without persisting credentials", async () => {
|
||||
const { root, target } = await createIdaTarget();
|
||||
|
|
@ -59,72 +126,10 @@ describe("IDA provider composition", () => {
|
|||
.find(({ operation }) => operation === "procedure_callers")?.available,
|
||||
).toBe(false);
|
||||
});
|
||||
it("re-observes externally mutable state even when a document is explicit and a snapshot is imported", async () => {
|
||||
const { root, target } = await createIdaTarget();
|
||||
roots.push(root);
|
||||
const path = join(root, "registration.json");
|
||||
await writeFile(path, JSON.stringify({ command: "fixture" }));
|
||||
const config = parseConfig({ REA_IDA_MCP_CONFIG: path });
|
||||
if (!config.ok) throw config.error;
|
||||
const producer = new RecordingIdaMcp(target);
|
||||
const session = createTestBinarySession(
|
||||
new IdaProvider(config.value, {}, () => producer),
|
||||
);
|
||||
expect((await session.open(target.path)).ok).toBe(true);
|
||||
const query = { procedure: "main", document: target.path };
|
||||
const first = await session.execute("procedure_pseudo_code", query);
|
||||
expect(first.ok && first.value.result).toContain("42");
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
if (!snapshot.ok) throw snapshot.error;
|
||||
expect(snapshot.value.entries).toEqual([]);
|
||||
const profile = session.analysisProfile();
|
||||
if (profile === undefined) throw new Error("Missing profile");
|
||||
const cached = createAnalysisExecution(
|
||||
"stale output",
|
||||
IDA_PROVIDER_IDENTITY,
|
||||
{
|
||||
analysisProfile: profile,
|
||||
subject: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
format: target.format,
|
||||
...(target.architecture === undefined
|
||||
? {}
|
||||
: { architecture: target.architecture }),
|
||||
},
|
||||
},
|
||||
);
|
||||
snapshot.value.entries.push(
|
||||
createAnalysisSnapshotEntry({
|
||||
target: snapshotTarget(target),
|
||||
binding: snapshotBinding(profile),
|
||||
operation: "procedure_pseudo_code",
|
||||
parameters: query,
|
||||
execution: cached,
|
||||
}),
|
||||
);
|
||||
snapshot.value.evidence_bundle = createEvidenceBundle([
|
||||
...snapshot.value.evidence_bundle.records,
|
||||
createEvidence(target, IDA_PROVIDER_IDENTITY, {
|
||||
operation: "procedure_pseudo_code",
|
||||
parameters: query,
|
||||
result: cached.result,
|
||||
rawResult: cached.rawResult,
|
||||
analysisProfile: profile,
|
||||
}),
|
||||
]);
|
||||
expect(session.importAnalysisSnapshot(snapshot.value).ok).toBe(true);
|
||||
expect(session.allowsSnapshotReplay("procedure_pseudo_code")).toBe(false);
|
||||
expect(session.allowsSnapshotReplay("binary_overview")).toBe(false);
|
||||
producer.pseudocode = "int main(void) { return 99; }";
|
||||
const second = await session.execute("procedure_pseudo_code", query);
|
||||
expect(second.ok && second.value.result).toContain("99");
|
||||
expect(
|
||||
producer.calls.filter(({ name }) => name === "decompile_function"),
|
||||
).toHaveLength(2);
|
||||
await session.close();
|
||||
expect(producer.closes).toBe(1);
|
||||
});
|
||||
it(
|
||||
"re-observes externally mutable state even when a document is explicit and a snapshot is imported",
|
||||
reobservesExternallyMutableState,
|
||||
);
|
||||
it("rejects unsupported target kinds and reports incomplete upstream profiles on first analysis", async () => {
|
||||
const { root, target } = await createIdaTarget();
|
||||
roots.push(root);
|
||||
|
|
|
|||
Loading…
Reference in New Issue