Skip to content

Commit bddbda3

Browse files
test: enforce tool_dispatch predicate ordering invariants (#98)
* 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. * test: harden predicate index lookup per review
1 parent 7f1e0cf commit bddbda3

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Structural ordering invariants for ``_TOOL_RESULT_DISPATCH``.
2+
3+
First matching predicate wins; misordering silently misclassifies tool results.
4+
Invariants are declared as ``(before, after, reason)`` triples — add a row to
5+
``ORDERING_INVARIANTS`` when inserting a predicate that must sit above another.
6+
"""
7+
8+
from collections.abc import Callable
9+
10+
import pytest
11+
12+
from models.tool_results import (
13+
is_file_write_tool_result,
14+
is_plan_tool_result,
15+
is_task_async_tool_result,
16+
is_task_completed_tool_result,
17+
is_task_message_tool_result,
18+
is_task_retrieval_tool_result,
19+
)
20+
from utils.tool_dispatch import _TOOL_RESULT_DISPATCH
21+
22+
Predicate = Callable[..., bool]
23+
24+
ORDERING_INVARIANTS: list[tuple[Predicate, Predicate, str]] = [
25+
(
26+
is_plan_tool_result,
27+
is_file_write_tool_result,
28+
"plan blobs may carry filePath + content; plan must win before file_write",
29+
),
30+
(
31+
is_task_message_tool_result,
32+
is_task_retrieval_tool_result,
33+
"task_message is broad (task_id or message); must precede narrower task_retrieval",
34+
),
35+
(
36+
is_task_message_tool_result,
37+
is_task_completed_tool_result,
38+
"task_message is broad (task_id or message); must precede narrower task_completed",
39+
),
40+
(
41+
is_task_message_tool_result,
42+
is_task_async_tool_result,
43+
"task_message is broad (task_id or message); must precede narrower task_async",
44+
),
45+
]
46+
47+
48+
def _predicate_index(predicate: Predicate) -> int:
49+
for i, entry in enumerate(_TOOL_RESULT_DISPATCH):
50+
pred = entry[0]
51+
# Identity match: dispatch table must store bare function refs (not wrappers).
52+
if pred is predicate:
53+
return i
54+
raise ValueError(f"predicate {predicate.__name__} not found in _TOOL_RESULT_DISPATCH")
55+
56+
57+
@pytest.mark.parametrize(
58+
"before,after,reason",
59+
ORDERING_INVARIANTS,
60+
ids=[
61+
"plan_before_file_write",
62+
"task_message_before_task_retrieval",
63+
"task_message_before_task_completed",
64+
"task_message_before_task_async",
65+
],
66+
)
67+
def test_tool_dispatch_ordering_invariant(
68+
before: Predicate,
69+
after: Predicate,
70+
reason: str,
71+
) -> None:
72+
before_idx = _predicate_index(before)
73+
after_idx = _predicate_index(after)
74+
assert before_idx < after_idx, (
75+
f"_TOOL_RESULT_DISPATCH ordering violation: "
76+
f"{before.__name__} (index {before_idx}) must precede "
77+
f"{after.__name__} (index {after_idx}). Reason: {reason}"
78+
)

utils/tool_dispatch.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
To add a shape: append ``(pred, build)`` at the end, or insert only after
1111
verifying predicates above would not steal intended matches.
1212
13+
Ordering invariants are enforced structurally by
14+
``tests/test_tool_dispatch_ordering.py`` — add a ``(before, after, reason)``
15+
tuple there when a new predicate must sit above another.
16+
1317
Predicates live in ``models.tool_results`` (single source of truth for narrowing).
1418
"""
1519

0 commit comments

Comments
 (0)