From f34249a2d855cdc596a6aa60e24e6217d0743fae Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 1 Oct 2026 15:11:28 -0400 Subject: [PATCH] test(rules): locate the pre-tool arm by what it does, not by its name (#4633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI run 712 failed test_neither_rule_arm_logs_its_call_behind_a_results_guard with "min() iterable argument is empty": the guard walked the function named build_tool_rule_hint, which since fd2ebf4 is a wrapper that searches nothing — the arm's body moved to _tool_rule_hint. The property it guards (the call is logged before any early return) still held; the guard had pinned a name. It now finds the async function that calls semantic_search_rules under the pre_tool_rule source, so a later rename moves the guard with the arm instead of emptying it (rule 167). Co-Authored-By: Claude Opus 5.5 --- tests/test_rule_usage_wiring.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/test_rule_usage_wiring.py b/tests/test_rule_usage_wiring.py index 6b466ae..91dde55 100644 --- a/tests/test_rule_usage_wiring.py +++ b/tests/test_rule_usage_wiring.py @@ -908,9 +908,23 @@ def test_neither_rule_arm_logs_its_call_behind_a_results_guard(): # # The property is positional: between the search and the first guard that # can return early, the call row has already been written. + # The arm's BODY, which since #4633 lives in `_tool_rule_hint`; the public + # `build_tool_rule_hint` is a wrapper that adds the via-lesson step and + # searches nothing itself. Located by what it does — the function that + # calls the rule search under the pre_tool_rule source — so the next + # rename moves the guard with it instead of emptying it. fn = next( n for n in ast.walk(ast.parse(pc_src)) - if isinstance(n, ast.AsyncFunctionDef) and n.name == "build_tool_rule_hint" + if isinstance(n, ast.AsyncFunctionDef) + and any( + isinstance(c, ast.Call) and getattr(c.func, "id", None) == "semantic_search_rules" + for c in ast.walk(n) + ) + and any( + isinstance(k, ast.keyword) and k.arg == "source" + and isinstance(k.value, ast.Constant) and k.value.value == "pre_tool_rule" + for k in ast.walk(n) + ) ) search_at = min( n.lineno for n in ast.walk(fn) @@ -929,7 +943,7 @@ def test_neither_rule_arm_logs_its_call_behind_a_results_guard(): and any(isinstance(b, ast.Return) for b in n.body) ] assert bailouts, ( - "no early return found after the search in build_tool_rule_hint — the " + "no early return found after the search in the pre-tool arm — the " "guard has nothing left to protect, which means this test is now " "passing vacuously rather than the arm being correct" )