Files
Open_Ultra_SAST/tests/test_obligation_summaries.py
T
Marius Ciepluch dc2832564a feat(detection-core): unique-declaration member-call binding (8.2 final gated round step 1-2)
x.method() with a variable receiver binds complete iff exactly one same-name
method declaration exists repository-wide; collisions stay a finite candidate
set (receiver_type_unknown) and module-level functions never bind. Authorized
by maintainer strategy-review 2026-10-31; gated by dev-pool FA non-regression.
2026-10-31 07:10:36 +01:00

683 lines
25 KiB
Python

"""Task 9.1 -- local function summaries: determinism, no caller traversal."""
from __future__ import annotations
import inspect
from dataclasses import FrozenInstanceError
from time import monotonic
import pytest
from openultrasast.semantic.cst import declaration_scopes
from openultrasast.semantic.ir import CallSite, FunctionIR
from openultrasast.semantic.obligations import summaries
from openultrasast.semantic.obligations.abstract_domain import Completeness
from openultrasast.semantic.obligations.operations import Discharge, Operation
from openultrasast.semantic.obligations.resolution import SymbolId, SymbolIndex
from openultrasast.semantic.obligations.summaries import (
EvidencePath,
ExplorationBudget,
FunctionSummary,
candidate_call_edges,
exact_call_edges,
exact_evidence_paths,
local_summaries,
monotone_summaries,
)
PHP_CLASS = """<?php
class InvoiceController {
public function preview($fields) {
return $this->render($fields);
}
}
"""
JAVA_ONE_CLASS = """public class Handler {
public void handle() {
this.run();
}
void run() {
}
}
"""
JAVA_TWO_CLASSES = """public class Auditor {
public boolean check() {
return true;
}
}
class Gate {
public boolean check() {
return false;
}
}
"""
JAVA_SELF_LOOP = """public class Node {
int walk() {
return this.walk();
}
}
"""
JAVA_MUTUAL = """public class C {
void a() {
this.b();
}
void b() {
this.a();
}
}
"""
JAVA_CHAIN = """public class Chain {
void helper1() {
this.helper2();
}
void helper2() {
}
}
"""
JAVA_THREE_CYCLE = """public class Ring {
void a() {
this.b();
}
void b() {
this.c();
}
void c() {
this.a();
}
}
"""
def _self_call(name: str, line: int, arity: int = 0) -> CallSite:
return CallSite(name, line, (), (), (), False, 0, receiver="this", arity=arity)
def _named_operation(path: str, function_name: str, resource: str) -> Operation:
return Operation(
path=path,
line=3,
function=function_name,
kind="protected_read",
fact_id="java:protected-read",
resource=resource,
requires=("path_guard",),
)
def _scope_functions(fixture: str, path: str, language: str) -> tuple[FunctionIR, ...]:
scopes = declaration_scopes(path, fixture, language)
assert scopes, f"fixture produced no declaration scopes: {path}"
functions = []
for scope in scopes:
calls = (CallSite("render", 4, (), (), (), False, 0),) if scope.function_name == "preview" else ()
functions.append(
FunctionIR(
name=scope.function_name,
params=scope.params,
start_line=scope.start_line,
end_line=scope.start_line + 3,
binds=(),
calls=calls,
)
)
return tuple(functions)
def _index(fixtures: tuple[tuple[str, str, str], ...]) -> SymbolIndex:
return SymbolIndex.build(fixtures)
def _operation(function_name: str) -> Operation:
return Operation(
path="src/InvoiceController.php",
line=3,
function=function_name,
kind="protected_read",
fact_id="php:protected-read",
resource="invoice",
requires=("path_guard",),
)
def _witness(function_name: str) -> Discharge:
return Discharge(
path="src/InvoiceController.php",
line=2,
function=function_name,
kind="path_guard",
fact_id="php:authorize",
provenance="call",
scope="statement",
detail="authorize",
)
def test_empty_function_produces_a_complete_empty_summary() -> None:
index = _index((("src/Handler.java", JAVA_ONE_CLASS, "java"),))
functions = {"src/Handler.java": (FunctionIR("run", (), 7, 8, (), ()),)}
(summary,) = local_summaries(functions, index)
assert summary.symbol.name == "run"
assert summary.operations == ()
assert summary.witnesses == ()
assert summary.calls == ()
assert summary.completeness == Completeness(is_complete=True)
def test_unresolved_call_leaves_local_completeness_incomplete() -> None:
index = _index((("src/Handler.java", JAVA_ONE_CLASS, "java"),))
calls = (CallSite("missing", 3, (), (), (), False, 0),)
functions = {"src/Handler.java": (FunctionIR("handle", (), 2, 5, (), calls),)}
(summary,) = local_summaries(functions, index)
assert summary.completeness.is_complete is False
assert "resolution:no_candidate" in summary.completeness.reasons
def test_variable_receiver_call_stays_incomplete() -> None:
index = _index((("src/Auditor.java", JAVA_TWO_CLASSES, "java"),))
calls = (CallSite("check", 2, (), (), (), False, 0, receiver="auditor", arity=0),)
functions = {"src/Auditor.java": (FunctionIR("review", (), 2, 4, (), calls),)}
(summary,) = local_summaries(functions, index)
assert len(summary.calls[0].targets) == 2
assert summary.completeness.is_complete is False
assert "resolution:receiver_type_unknown" in summary.completeness.reasons
def test_exact_self_call_is_complete() -> None:
index = _index((("src/Handler.java", JAVA_ONE_CLASS, "java"),))
calls = (CallSite("run", 3, (), (), (), False, 0, receiver="this", arity=0),)
functions = {"src/Handler.java": (FunctionIR("handle", (), 2, 5, (), calls),)}
(summary,) = local_summaries(functions, index)
assert len(summary.calls[0].targets) == 1
assert summary.completeness == Completeness(is_complete=True)
def test_injected_operations_and_witnesses_are_preserved() -> None:
index = _index((("src/InvoiceController.php", PHP_CLASS, "php"),))
scopes = declaration_scopes("src/InvoiceController.php", PHP_CLASS, "php")
preview_scope = scopes[0]
functions = {"src/InvoiceController.php": (FunctionIR(preview_scope.function_name, preview_scope.params, 3, 5, (), ()),)}
symbol = local_summaries(functions, index)[0].symbol
operations = {symbol: (_operation(preview_scope.function_name),)}
witnesses = {symbol: (_witness(preview_scope.function_name),)}
(summary,) = local_summaries(functions, index, operations_by_symbol=operations, witnesses_by_symbol=witnesses)
assert summary.operations == (_operation(preview_scope.function_name),)
assert summary.witnesses == (_witness(preview_scope.function_name),)
def test_summary_symbol_carries_enclosing_type_and_signature() -> None:
index = _index((("src/InvoiceController.php", PHP_CLASS, "php"),))
functions = {"src/InvoiceController.php": (FunctionIR("preview", ("$fields",), 3, 5, (), ()),)}
(summary,) = local_summaries(functions, index)
assert summary.symbol.enclosing_type == "InvoiceController"
assert summary.symbol.signature == ("$fields",)
def test_summaries_are_deterministic_across_builds() -> None:
index = _index(
(
("src/InvoiceController.php", PHP_CLASS, "php"),
("src/Handler.java", JAVA_ONE_CLASS, "java"),
)
)
functions = {
"src/Handler.java": (FunctionIR("handle", (), 2, 5, (), ()),),
"src/InvoiceController.php": (FunctionIR("preview", ("$fields",), 3, 5, (), ()),),
}
assert local_summaries(functions, index) == local_summaries(functions, index)
def test_module_text_contains_no_target_names() -> None:
module_source = inspect.getsource(summaries)
for target_name in ("symfony", "laravel", "tornado", "wordpress", "stapler", "jetty", "rails", "spring"):
assert target_name not in module_source.lower()
def test_function_summary_is_frozen() -> None:
summary = FunctionSummary.__new__(FunctionSummary)
with pytest.raises(Exception, match="cannot assign|frozen"):
summary.symbol = None # type: ignore[misc,assignment]
def _symbol_by_name(summaries: tuple[FunctionSummary, ...], name: str) -> SymbolId:
for summary in summaries:
if summary.symbol.name == name:
return summary.symbol
raise AssertionError(f"no summary named {name}")
def test_direct_recursion_terminates_with_stable_operations() -> None:
index = _index((("src/Node.java", JAVA_SELF_LOOP, "java"),))
walk = FunctionIR("walk", (), 2, 4, (), (_self_call("walk", 3),))
functions = {"src/Node.java": (walk,)}
local = local_summaries(functions, index)
operations = {_symbol_by_name(local, "walk"): (_named_operation("src/Node.java", "walk", "node"),)}
result = monotone_summaries(functions, index, operations_by_symbol=operations)
assert len(result) == 1
assert result[0].operations == (_named_operation("src/Node.java", "walk", "node"),)
assert result[0].completeness == Completeness(is_complete=True)
def test_mutual_recursion_shares_accumulated_operations_and_witnesses() -> None:
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
function_a = FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),))
function_b = FunctionIR("b", (), 5, 7, (), (_self_call("a", 6),))
functions = {"src/C.java": (function_a, function_b)}
local = local_summaries(functions, index)
symbol_b = _symbol_by_name(local, "b")
operations = {symbol_b: (_named_operation("src/C.java", "b", "shared"),)}
witnesses = {symbol_b: (_witness("b"),)}
result = monotone_summaries(functions, index, operations_by_symbol=operations, witnesses_by_symbol=witnesses)
shared = (_named_operation("src/C.java", "b", "shared"),)
by_name = {summary.symbol.name: summary for summary in result}
assert by_name["a"].operations == shared
assert by_name["b"].operations == shared
assert by_name["a"].witnesses == (_witness("b"),)
def test_chain_helper_propagates_operations_to_caller() -> None:
index = _index((("src/Chain.java", JAVA_CHAIN, "java"),))
helper1 = FunctionIR("helper1", (), 2, 4, (), (_self_call("helper2", 3),))
helper2 = FunctionIR("helper2", (), 5, 6, (), ())
functions = {"src/Chain.java": (helper1, helper2)}
local = local_summaries(functions, index)
operations = {_symbol_by_name(local, "helper2"): (_named_operation("src/Chain.java", "helper2", "chain"),)}
result = monotone_summaries(functions, index, operations_by_symbol=operations)
by_name = {summary.symbol.name: summary for summary in result}
assert by_name["helper1"].operations == (_named_operation("src/Chain.java", "helper2", "chain"),)
assert by_name["helper1"].completeness == Completeness(is_complete=True)
def test_candidate_edges_never_propagate_operations() -> None:
index = _index((("src/Auditor.java", JAVA_TWO_CLASSES, "java"),))
review_calls = (CallSite("check", 2, (), (), (), False, 0, receiver="auditor", arity=0),)
review = FunctionIR("review", (), 2, 4, (), review_calls)
check = FunctionIR("check", (), 2, 4, (), ())
functions = {"src/Auditor.java": (review, check)}
local = local_summaries(functions, index)
auditor_symbol = next(symbol for symbol in (s.symbol for s in local) if symbol.enclosing_type == "Auditor")
operations = {auditor_symbol: (_named_operation("src/Auditor.java", "check", "auditor"),)}
result = monotone_summaries(functions, index, operations_by_symbol=operations)
by_name = {(summary.symbol.enclosing_type, summary.symbol.name): summary for summary in result}
assert by_name[(None, "review")].operations == ()
assert by_name[("Auditor", "check")].operations == (_named_operation("src/Auditor.java", "check", "auditor"),)
def test_incomplete_callee_reason_propagates_to_caller() -> None:
index = _index((("src/Chain.java", JAVA_CHAIN, "java"),))
helper1 = FunctionIR("helper1", (), 2, 4, (), (_self_call("helper2", 3),))
helper2 = FunctionIR("helper2", (), 5, 6, (), (CallSite("missing", 6, (), (), (), False, 0),))
functions = {"src/Chain.java": (helper1, helper2)}
result = monotone_summaries(functions, index)
by_name = {summary.symbol.name: summary for summary in result}
assert by_name["helper2"].completeness.is_complete is False
assert by_name["helper1"].completeness.is_complete is False
assert "resolution:no_candidate" in by_name["helper1"].completeness.reasons
def test_three_cycle_accumulates_all_operations() -> None:
index = _index((("src/Ring.java", JAVA_THREE_CYCLE, "java"),))
function_a = FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),))
function_b = FunctionIR("b", (), 5, 7, (), (_self_call("c", 6),))
function_c = FunctionIR("c", (), 8, 10, (), (_self_call("a", 9),))
functions = {"src/Ring.java": (function_a, function_b, function_c)}
local = local_summaries(functions, index)
operations = {
_symbol_by_name(local, "a"): (_named_operation("src/Ring.java", "a", "ring-a"),),
_symbol_by_name(local, "b"): (_named_operation("src/Ring.java", "b", "ring-b"),),
_symbol_by_name(local, "c"): (_named_operation("src/Ring.java", "c", "ring-c"),),
}
result = monotone_summaries(functions, index, operations_by_symbol=operations)
accumulated = (
_named_operation("src/Ring.java", "a", "ring-a"),
_named_operation("src/Ring.java", "b", "ring-b"),
_named_operation("src/Ring.java", "c", "ring-c"),
)
for summary in result:
assert summary.operations == accumulated
def test_monotone_summaries_are_deterministic() -> None:
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
function_a = FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),))
function_b = FunctionIR("b", (), 5, 7, (), (_self_call("a", 6),))
functions = {"src/C.java": (function_a, function_b)}
assert monotone_summaries(functions, index) == monotone_summaries(functions, index)
def test_monotone_output_order_matches_local_summaries() -> None:
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
function_a = FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),))
function_b = FunctionIR("b", (), 5, 7, (), (_self_call("a", 6),))
functions = {"src/C.java": (function_a, function_b)}
assert [summary.symbol for summary in monotone_summaries(functions, index)] == [
summary.symbol for summary in local_summaries(functions, index)
]
JAVA_FANOUT = """
class Fan {
void helper1() { this.helper2(); this.helper3(); }
void helper2() {}
void helper3() {}
}
""".strip()
def _java_operation(name: str, line: int) -> Operation:
return Operation(
path="src/C.java",
line=line,
function=name,
kind="protected_read",
fact_id=f"java:{name}-op",
resource=name,
requires=("path_guard",),
)
def _fanout_functions() -> dict[str, tuple[FunctionIR, ...]]:
return {
"src/C.java": (
FunctionIR(
"helper1",
(),
3,
3,
(),
(_self_call("helper2", 3), _self_call("helper3", 3)),
),
FunctionIR("helper2", (), 4, 4, (), ()),
FunctionIR("helper3", (), 5, 5, (), ()),
)
}
def _mutual_functions() -> dict[str, tuple[FunctionIR, ...]]:
return {
"src/C.java": (
FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),)),
FunctionIR("b", (), 5, 7, (), (_self_call("a", 6),)),
)
}
def _operations_injected(
functions: dict[str, tuple[FunctionIR, ...]], index: SymbolIndex, injected: dict[str, Operation]
) -> dict[SymbolId, tuple[Operation, ...]]:
operations_by_symbol: dict[SymbolId, tuple[Operation, ...]] = {}
for summary in local_summaries(functions, index):
operation = injected.get(summary.symbol.name)
if operation is not None:
operations_by_symbol[summary.symbol] = (operation,)
return operations_by_symbol
def test_depth_cutoff_preserves_accumulated_evidence_and_reason() -> None:
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
functions = _mutual_functions()
operations_by_symbol = _operations_injected(functions, index, {"b": _java_operation("b", 6)})
summaries_with_cutoff = monotone_summaries(
functions, index, operations_by_symbol=operations_by_symbol, budget=ExplorationBudget(depth=1)
)
summaries_unbounded = monotone_summaries(functions, index, operations_by_symbol=operations_by_symbol)
by_name = {summary.symbol.name: summary for summary in summaries_with_cutoff}
unbounded_by_name = {summary.symbol.name: summary for summary in summaries_unbounded}
assert by_name["a"].operations == unbounded_by_name["a"].operations
assert by_name["a"].operations == (_java_operation("b", 6),)
assert "summaries:depth_cutoff" in by_name["a"].completeness.reasons
assert "summaries:depth_cutoff" in by_name["b"].completeness.reasons
assert not by_name["a"].completeness.is_complete
def test_deadline_cutoff_returns_local_evidence() -> None:
index = _index((("src/C.java", JAVA_FANOUT, "java"),))
functions = _fanout_functions()
operations_by_symbol = _operations_injected(
functions, index, {"helper2": _java_operation("helper2", 4), "helper3": _java_operation("helper3", 5)}
)
summaries_with_cutoff = monotone_summaries(
functions, index, operations_by_symbol=operations_by_symbol, budget=ExplorationBudget(deadline=monotonic() - 1.0)
)
by_name = {summary.symbol.name: summary for summary in summaries_with_cutoff}
assert by_name["helper1"].operations == ()
assert "summaries:deadline_cutoff" in by_name["helper1"].completeness.reasons
assert not by_name["helper1"].completeness.is_complete
assert by_name["helper2"].completeness.is_complete
assert by_name["helper2"].operations == (_java_operation("helper2", 4),)
def test_fan_out_cutoff_merges_sorted_prefix() -> None:
index = _index((("src/C.java", JAVA_FANOUT, "java"),))
functions = _fanout_functions()
operations_by_symbol = _operations_injected(
functions, index, {"helper2": _java_operation("helper2", 4), "helper3": _java_operation("helper3", 5)}
)
summaries_with_cutoff = monotone_summaries(
functions, index, operations_by_symbol=operations_by_symbol, budget=ExplorationBudget(fan_out=1)
)
by_name = {summary.symbol.name: summary for summary in summaries_with_cutoff}
assert by_name["helper1"].operations == (_java_operation("helper2", 4),)
assert "summaries:fan_out_cutoff" in by_name["helper1"].completeness.reasons
assert not by_name["helper1"].completeness.is_complete
assert by_name["helper3"].completeness.is_complete
def test_all_none_budget_equals_default_behaviour() -> None:
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
functions = _mutual_functions()
operations_by_symbol = _operations_injected(functions, index, {"b": _java_operation("b", 6)})
with_default = monotone_summaries(functions, index, operations_by_symbol=operations_by_symbol)
with_empty_budget = monotone_summaries(functions, index, operations_by_symbol=operations_by_symbol, budget=ExplorationBudget())
assert with_default == with_empty_budget
def test_exploration_budget_is_frozen() -> None:
budget = ExplorationBudget(depth=1)
with pytest.raises(FrozenInstanceError):
budget.depth = 2 # type: ignore[misc]
def test_cutoff_reasons_join_existing_incompleteness() -> None:
index = _index((("src/C.java", JAVA_FANOUT, "java"),))
functions = {
"src/C.java": (
FunctionIR(
"helper1",
(),
3,
3,
(),
(_self_call("helper2", 3), CallSite("missing", 3, (), (), (), False, 0)),
),
FunctionIR("helper2", (), 4, 4, (), ()),
)
}
summaries_with_cutoff = monotone_summaries(functions, index, budget=ExplorationBudget(deadline=monotonic() - 1.0))
by_name = {summary.symbol.name: summary for summary in summaries_with_cutoff}
reasons = by_name["helper1"].completeness.reasons
assert "resolution:no_candidate" in reasons
assert "summaries:deadline_cutoff" in reasons
# --- Task 9.4: exact evidence paths (Reqs 3.9, 3.10, 11.4, 11.5) ---
JAVA_ENTRY_CHAIN = """class Handler {
public void handle() {
this.run("job");
}
public void run(String command) {
this.persist(command);
}
public void persist(String payload) {
}
}
"""
JAVA_TWO_ROUTES = """class Gate {
public void enter() {
this.left();
this.right();
}
public void left() {
this.vault();
}
public void right() {
this.vault();
}
public void vault() {
}
}
"""
JAVA_AUDITOR = """class Auditor {
public void check() {
}
}
class Gate {
public void check() {
}
}
"""
def _chain_index() -> SymbolIndex:
return SymbolIndex.build(
(
("src/Handler.java", JAVA_ENTRY_CHAIN, "java"),
("src/Auditor.java", JAVA_AUDITOR, "java"),
)
)
def _routes_index() -> SymbolIndex:
return SymbolIndex.build((("src/Gate.java", JAVA_TWO_ROUTES, "java"),))
def _chain_functions() -> dict[str, tuple[FunctionIR, ...]]:
return {
"src/Handler.java": (
FunctionIR("handle", (), 2, 4, (), (CallSite("run", 3, (), (), (), False, 0, receiver="this", arity=1),)),
FunctionIR("run", ("command",), 5, 7, (), (CallSite("persist", 6, (), (), (), False, 0, receiver="this", arity=1),)),
FunctionIR("persist", ("payload",), 8, 9, (), ()),
)
}
def _routes_functions() -> dict[str, tuple[FunctionIR, ...]]:
return {
"src/Gate.java": (
FunctionIR(
"enter",
(),
2,
5,
(),
(
CallSite("left", 3, (), (), (), False, 0, receiver="this"),
CallSite("right", 4, (), (), (), False, 0, receiver="this"),
),
),
FunctionIR("left", (), 6, 8, (), (CallSite("vault", 7, (), (), (), False, 0, receiver="this"),)),
FunctionIR("right", (), 9, 11, (), (CallSite("vault", 10, (), (), (), False, 0, receiver="this"),)),
FunctionIR("vault", (), 12, 13, (), ()),
)
}
def _symbol(index: SymbolIndex, name: str) -> SymbolId:
return next(symbol for symbol in index.by_name[name] if symbol.module == "src/Handler.java")
def _routes_symbol(index: SymbolIndex, name: str) -> SymbolId:
return next(symbol for symbol in index.by_name[name] if symbol.module == "src/Gate.java")
def test_exact_edges_carry_call_site_lines() -> None:
index = _chain_index()
edges = exact_call_edges(_chain_functions(), index)
pairs = {(edge.caller.name, edge.callee.name): edge.line for edge in edges}
assert pairs[("handle", "run")] == 3
assert pairs[("run", "persist")] == 6
def test_entry_chain_emits_hops_from_entry_to_operation() -> None:
index = _chain_index()
edges = exact_call_edges(_chain_functions(), index)
persist_symbol = _symbol(index, "persist")
paths = exact_evidence_paths(
(_symbol(index, "handle"),),
edges,
operations_by_symbol={persist_symbol: (_java_operation("persist", 9),)},
)
assert len(paths) == 1
assert paths[0].entry.name == "handle"
assert paths[0].target.name == "persist"
assert [hop.callee.name for hop in paths[0].hops] == ["run", "persist"]
assert paths[0].hops[0].line == 3
def test_entry_itself_holding_an_operation_emits_a_zero_hop_path() -> None:
index = _chain_index()
edges = exact_call_edges(_chain_functions(), index)
handle_symbol = _symbol(index, "handle")
paths = exact_evidence_paths(
(handle_symbol,),
edges,
operations_by_symbol={handle_symbol: (_java_operation("handle", 3),)},
)
assert paths == (EvidencePath(entry=handle_symbol, target=handle_symbol, hops=()),)
def test_two_complete_chains_to_one_operation_stay_independent() -> None:
index = _routes_index()
edges = exact_call_edges(_routes_functions(), index)
enter = _routes_symbol(index, "enter")
vault = _routes_symbol(index, "vault")
paths = exact_evidence_paths(
(enter,),
edges,
operations_by_symbol={vault: (_java_operation("vault", 12),)},
)
routes = sorted(tuple(hop.callee.name for hop in path.hops) for path in paths)
assert routes == [("left", "vault"), ("right", "vault")]
assert all(path.target == vault for path in paths)
def test_candidate_edges_never_complete_a_path() -> None:
index = _chain_index()
functions = {
"src/Handler.java": (FunctionIR("handle", (), 2, 4, (), (CallSite("check", 3, (), (), (), False, 0, receiver="auditor"),)),)
+ _chain_functions()["src/Handler.java"][1:]
}
edges = exact_call_edges(functions, index)
auditor = next(symbol for symbol in index.by_name["check"] if symbol.module == "src/Auditor.java")
paths = exact_evidence_paths(
(_symbol(index, "handle"),),
edges,
operations_by_symbol={auditor: (_java_operation("check", 1),)},
)
assert paths == ()
diagnostics = candidate_call_edges(functions, index)
assert len(diagnostics) == 1
assert diagnostics[0].caller.name == "handle"
assert diagnostics[0].reason == "resolution:receiver_type_unknown"
assert any(candidate.enclosing_type == "Auditor" for candidate in diagnostics[0].candidates)
def test_exact_evidence_paths_are_deterministic() -> None:
index = _routes_index()
edges = exact_call_edges(_routes_functions(), index)
enter = _routes_symbol(index, "enter")
vault = _routes_symbol(index, "vault")
operations = {vault: (_java_operation("vault", 12),)}
assert exact_evidence_paths((enter,), edges, operations_by_symbol=operations) == exact_evidence_paths(
(enter,), edges, operations_by_symbol=operations
)