fix(hopper): preserve unknown relationship completeness

This commit is contained in:
morluto committed 2026-10-24 04:19:12 +08:00
1 parent ecfac46c8d
commit 23664efdbb
3 files changed
+74 -3

No files matched your search

+55 -1
View File
@@ -1,7 +1,9 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { parseConfig } from "../config/parseConfig.js";
import { silentLogger } from "../logger.js";
import { ok } from "../domain/result.js";
import { HopperClient } from "./HopperClient.js";
import { HopperProvider } from "./HopperProvider.js";
describe("Hopper provider capabilities", () => {
@@ -86,3 +88,55 @@ describe("Hopper provider capabilities", () => {
});
});
});
describe("Hopper relationship coverage", () => {
it.each([
["procedure_callers", ["callers"]],
["procedure_callees", ["callees"]],
["analyze_function", ["callers", "callees"]],
] as const)(
"retains %s observations without claiming exhaustive relationships",
async (operation, directions) => {
const config = parseConfig({});
if (!config.ok) throw config.error;
const source =
operation === "analyze_function"
? {
callers: [],
callees: [{ address: "0x2000", name: "tail_target" }],
}
: operation === "procedure_callees"
? ["0x2000"]
: [];
const callTool = vi
.spyOn(HopperClient.prototype, "callTool")
.mockResolvedValue(ok(source));
const client = new HopperProvider(
config.value,
silentLogger,
{},
"linux",
).createClient({
path: "/tmp/fixture.hop",
sha256: "a".repeat(64),
kind: "database",
format: "analysis-database",
});
try {
const observed = await client.execute(operation, {
procedure: "0x1000",
});
if (!observed.ok) throw observed.error;
expect(observed.value.result).toEqual(source);
expect(observed.value.relationshipCoverage).toMatchObject({
status: "unknown",
limited_directions: directions,
reason: expect.stringContaining("resolved tail calls"),
});
} finally {
await client.close();
callTool.mockRestore();
}
},
);
});
+13 -1
View File
@@ -347,7 +347,19 @@ export class HopperProvider implements AnalysisProviderCandidate {
...(operation === "procedure_callers" ||
operation === "procedure_callees" ||
operation === "analyze_function"
? { relationshipCoverage: { status: "complete" as const } }
? {
relationshipCoverage: {
status: "unknown" as const,
limited_directions:
operation === "procedure_callers"
? (["callers"] as const)
: operation === "procedure_callees"
? (["callees"] as const)
: (["callers", "callees"] as const),
reason:
"Hopper reports caller and callee procedures through independent public API inventories. Their completeness, including resolved tail calls and agreement between reverse and forward relationships, has not been established. Observed relationships are retained.",
},
}
: {}),
limitations: [
...(target.kind !== "executable"