From b83bce58d6c00805f900f988c1535c8ab3344ca8 Mon Sep 17 00:00:00 2001 From: chen Date: Fri, 26 Jun 2026 00:15:29 +0800 Subject: [PATCH 1/2] test: enforce tool_dispatch predicate ordering invariants Add declarative (before, after, reason) ordering checks so misordered predicates in _TOOL_RESULT_DISPATCH fail immediately with a clear message. --- tests/test_tool_dispatch_ordering.py | 76 ++++++++++++++++++++++++++++ utils/tool_dispatch.py | 4 ++ 2 files changed, 80 insertions(+) create mode 100644 tests/test_tool_dispatch_ordering.py diff --git a/tests/test_tool_dispatch_ordering.py b/tests/test_tool_dispatch_ordering.py new file mode 100644 index 0000000..8da2d3f --- /dev/null +++ b/tests/test_tool_dispatch_ordering.py @@ -0,0 +1,76 @@ +"""Structural ordering invariants for ``_TOOL_RESULT_DISPATCH``. + +First matching predicate wins; misordering silently misclassifies tool results. +Invariants are declared as ``(before, after, reason)`` triples — add a row to +``ORDERING_INVARIANTS`` when inserting a predicate that must sit above another. +""" + +from collections.abc import Callable + +import pytest + +from models.tool_results import ( + is_file_write_tool_result, + is_plan_tool_result, + is_task_async_tool_result, + is_task_completed_tool_result, + is_task_message_tool_result, + is_task_retrieval_tool_result, +) +from utils.tool_dispatch import _TOOL_RESULT_DISPATCH + +Predicate = Callable[..., bool] + +ORDERING_INVARIANTS: list[tuple[Predicate, Predicate, str]] = [ + ( + is_plan_tool_result, + is_file_write_tool_result, + "plan blobs may carry filePath + content; plan must win before file_write", + ), + ( + is_task_message_tool_result, + is_task_retrieval_tool_result, + "task_message is broad (task_id or message); must precede narrower task_retrieval", + ), + ( + is_task_message_tool_result, + is_task_completed_tool_result, + "task_message is broad (task_id or message); must precede narrower task_completed", + ), + ( + is_task_message_tool_result, + is_task_async_tool_result, + "task_message is broad (task_id or message); must precede narrower task_async", + ), +] + + +def _predicate_index(predicate: Predicate) -> int: + for i, (pred, _) in enumerate(_TOOL_RESULT_DISPATCH): + if pred is predicate: + return i + raise ValueError(f"predicate {predicate.__name__} not found in _TOOL_RESULT_DISPATCH") + + +@pytest.mark.parametrize( + "before,after,reason", + ORDERING_INVARIANTS, + ids=[ + "plan_before_file_write", + "task_message_before_task_retrieval", + "task_message_before_task_completed", + "task_message_before_task_async", + ], +) +def test_tool_dispatch_ordering_invariant( + before: Predicate, + after: Predicate, + reason: str, +) -> None: + before_idx = _predicate_index(before) + after_idx = _predicate_index(after) + assert before_idx < after_idx, ( + f"_TOOL_RESULT_DISPATCH ordering violation: " + f"{before.__name__} (index {before_idx}) must precede " + f"{after.__name__} (index {after_idx}). Reason: {reason}" + ) diff --git a/utils/tool_dispatch.py b/utils/tool_dispatch.py index 03081c2..5e836c1 100644 --- a/utils/tool_dispatch.py +++ b/utils/tool_dispatch.py @@ -10,6 +10,10 @@ To add a shape: append ``(pred, build)`` at the end, or insert only after verifying predicates above would not steal intended matches. +Ordering invariants are enforced structurally by +``tests/test_tool_dispatch_ordering.py`` — add a ``(before, after, reason)`` +tuple there when a new predicate must sit above another. + Predicates live in ``models.tool_results`` (single source of truth for narrowing). """ From e351ee7676f611142b245aa6f920da9857a05528 Mon Sep 17 00:00:00 2001 From: chen Date: Fri, 26 Jun 2026 02:16:19 +0800 Subject: [PATCH 2/2] test: harden predicate index lookup per review --- tests/test_tool_dispatch_ordering.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_tool_dispatch_ordering.py b/tests/test_tool_dispatch_ordering.py index 8da2d3f..ce6835e 100644 --- a/tests/test_tool_dispatch_ordering.py +++ b/tests/test_tool_dispatch_ordering.py @@ -46,7 +46,9 @@ def _predicate_index(predicate: Predicate) -> int: - for i, (pred, _) in enumerate(_TOOL_RESULT_DISPATCH): + for i, entry in enumerate(_TOOL_RESULT_DISPATCH): + pred = entry[0] + # Identity match: dispatch table must store bare function refs (not wrappers). if pred is predicate: return i raise ValueError(f"predicate {predicate.__name__} not found in _TOOL_RESULT_DISPATCH")