fix(call-path): keep caller-only unknown coverage from blocking not_found (#2371)

* fix(call-path): keep caller-only unknown coverage from blocking not_found

build_call_path expands only callee edges, so relationship coverage that
is unknown solely because callers are unobservable (the modern headless
IDA profile) no longer marks the searched callee closure incomplete.
The caller caveat stays visible in limitations, and callee-side unknowns
still yield unknown. Fixes #2322.

* test(call-path): cover IDA-capped callees beside unknown callers

Greptile review noted the poll-timing coverage only exercised unknown
callees, leaving the partial callees IDA returns at its 500-result cap
untested. Add the combined unknown-callers/partial-callees case:
isCallerOnlyUnknownCoverage must stay false and buildCallPath must keep
search_scope.exhaustive false, so a future change cannot silently report
not_found when IDA has omitted callees.
This commit is contained in:
Saim Shafique 2026-10-13 17:42:40 +05:00 committed by GitHub
parent ec4f4ddc58
commit 84cf7791a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 173 additions and 3 deletions

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { buildCallPath } from "./callPath.js";
import { combineFunctionRelationshipCoverage } from "./functionCallCoverage.js";
import { callPathResultSchema, type CallPathInput } from "./callPathSchemas.js";
import { createEvidence, type Evidence } from "./evidence.js";
import type { JsonValue } from "./jsonValue.js";
@ -396,6 +397,60 @@ describe("explicit call relationship coverage", () => {
expect(result.limitations).toEqual([reason]);
});
it("establishes not_found when only caller coverage is unknown", () => {
const reason =
"Callers: Upstream xrefs do not establish direct caller relationships.";
const result = run([
observe("0x1000", ["0x2000"], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
observe("0x2000", [], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
]);
expect(result).toMatchObject({
status: "not_found",
search_scope: { exhaustive: true },
});
expect(result.limitations).toContain(reason);
});
it("stays unknown when callee coverage is unknown", () => {
const reason =
"Callees: Upstream callees capped results at 500; additional callees remain unknown.";
const result = run([
observe("0x1000", ["0x2000"], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
observe("0x2000", [], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
]);
expect(result).toMatchObject({
status: "unknown",
search_scope: { exhaustive: false },
});
expect(result.limitations).toContain(reason);
});
it("stays unknown when both caller and callee coverage are unknown", () => {
const reason =
"Callers: Upstream xrefs do not establish direct caller relationships. Callees: Upstream callees are not observed.";
const result = run([
observe("0x1000", ["0x2000"], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
observe("0x2000", [], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
]);
expect(result).toMatchObject({
status: "unknown",
search_scope: { exhaustive: false },
});
expect(result.limitations).toContain(reason);
});
it("ignores incomplete relationship coverage outside the reachable search closure", () => {
const reason = "An unrelated function's callees were not observed.";
const result = run([
@ -415,6 +470,34 @@ describe("explicit call relationship coverage", () => {
});
});
describe("IDA-capped callee coverage beside unknown callers", () => {
it("keeps the search non-exhaustive for the combined coverage value", () => {
const combined = combineFunctionRelationshipCoverage(
{
status: "unknown",
reason: "Upstream xrefs do not establish direct caller relationships.",
},
{
status: "partial",
limitations: [
"Upstream callees capped results at 500; additional callees remain unknown.",
],
},
);
expect(combined.status).toBe("unknown");
const result = run([
observe("0x1000", [], "rea-workflow", { relationshipCoverage: combined }),
observe("0x4000", []),
]);
expect(result).toMatchObject({
status: "unknown",
search_scope: { exhaustive: false },
shortest_hops: null,
paths: [],
});
});
});
describe("call path evidence admission", () => {
it("normalizes hex addresses and rejects duplicates and mixed providers", () => {
expect(

View File

@ -10,6 +10,7 @@ import {
parseFunctionEvidence,
type FunctionSnapshot,
} from "./functionDossierEvidence.js";
import { isCallerOnlyUnknownCoverage } from "./functionCallCoverage.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
/** Minimal directed caller-to-callee adjacency for call-path search. */
@ -255,10 +256,14 @@ const inspectSearch = ({
// Explicit, Evidence-bound relationship coverage owns completeness;
// generic provider/dossier limitations remain contextual. Legacy Evidence
// without that metadata retains its conservative limitation-based rule.
// The search only expands callee edges, so coverage that is unknown
// solely because callers are unobservable does not make the searched
// callee closure incomplete; the caller caveat stays in limitations.
if (
coverage === null
? snapshot.limitations.length > 0
: coverage.status !== "complete"
: coverage.status !== "complete" &&
!isCallerOnlyUnknownCoverage(coverage)
)
incompleteExpandedCoverage = true;
} else if (node === goal) {

View File

@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import {
combineFunctionRelationshipCoverage,
isCallerOnlyUnknownCoverage,
} from "./functionCallCoverage.js";
import type { RelationshipCoverage } from "./relationshipCoverage.js";
const unknownCallers: RelationshipCoverage = {
status: "unknown",
reason: "Upstream xrefs do not establish direct caller relationships.",
};
const unknownCallees: RelationshipCoverage = {
status: "unknown",
reason: "Upstream callees are not observed in this profile.",
};
const cappedCallees: RelationshipCoverage = {
status: "partial",
limitations: [
"Upstream callees capped results at 500; additional callees remain unknown.",
],
};
const complete: RelationshipCoverage = { status: "complete" };
describe("isCallerOnlyUnknownCoverage", () => {
it("detects unknown callers beside complete callees", () => {
const combined = combineFunctionRelationshipCoverage(
unknownCallers,
complete,
);
expect(combined.status).toBe("unknown");
expect(isCallerOnlyUnknownCoverage(combined)).toBe(true);
});
it("rejects unknown callees beside complete callers", () => {
const combined = combineFunctionRelationshipCoverage(
complete,
unknownCallees,
);
expect(isCallerOnlyUnknownCoverage(combined)).toBe(false);
});
it("rejects unknown coverage on both sides", () => {
const combined = combineFunctionRelationshipCoverage(
unknownCallers,
unknownCallees,
);
expect(isCallerOnlyUnknownCoverage(combined)).toBe(false);
});
it("rejects unknown callers beside IDA-capped callees", () => {
const combined = combineFunctionRelationshipCoverage(
unknownCallers,
cappedCallees,
);
expect(combined.status).toBe("unknown");
expect(isCallerOnlyUnknownCoverage(combined)).toBe(false);
});
it("rejects complete coverage", () => {
const combined = combineFunctionRelationshipCoverage(complete, complete);
expect(isCallerOnlyUnknownCoverage(combined)).toBe(false);
});
});

View File

@ -2,6 +2,9 @@ import type { RelationshipCoverage } from "./relationshipCoverage.js";
import type { FunctionDossier } from "./hopperValues.js";
import { uniqueSorted } from "./canonicalOrdering.js";
const callerCoveragePrefix = "Callers: ";
const calleeCoveragePrefix = "Callees: ";
/** Combine caller and callee read outcomes without losing either explanation. */
export const combineFunctionRelationshipCoverage = (
callers: RelationshipCoverage | undefined,
@ -20,7 +23,8 @@ export const combineFunctionRelationshipCoverage = (
);
const details = diagnostics.length > 0 ? { diagnostics } : {};
const reasons = reads.flatMap(({ direction, coverage }) => {
const label = direction === "callers" ? "Callers" : "Callees";
const label =
direction === "callers" ? callerCoveragePrefix : calleeCoveragePrefix;
const limitations =
coverage === undefined
? ["Provider did not report relationship coverage."]
@ -29,7 +33,7 @@ export const combineFunctionRelationshipCoverage = (
: coverage.status === "partial"
? coverage.limitations
: [];
return limitations.map((reason) => `${label}: ${reason}`);
return limitations.map((reason) => `${label}${reason}`);
});
if (
reads.some(
@ -50,6 +54,20 @@ export const combineFunctionRelationshipCoverage = (
return { status: "complete", ...details };
};
/**
* Whether combined relationship coverage is unknown only on the caller side.
* The combine helper above prefixes each direction's reasons, so a reason
* that carries the caller prefix and no callee prefix records observed
* callees with unobservable callers. Forward (callee-edge) searches stay
* exhaustive under that caveat; the caller reason remains a limitation.
*/
export const isCallerOnlyUnknownCoverage = (
coverage: RelationshipCoverage,
): boolean =>
coverage.status === "unknown" &&
coverage.reason.startsWith(callerCoveragePrefix) &&
!coverage.reason.includes(calleeCoveragePrefix);
/** Explain why two resolved call sets cannot establish call-set equality. */
export const functionCallCoverageLimitations = (
coverage: RelationshipCoverage | null,