Skip to content

Commit b83bce5

Browse files
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.
1 parent 7f1e0cf commit b83bce5

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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, (pred, _) in enumerate(_TOOL_RESULT_DISPATCH):
50+
if pred is predicate:
51+
return i
52+
raise ValueError(f"predicate {predicate.__name__} not found in _TOOL_RESULT_DISPATCH")
53+
54+
55+
@pytest.mark.parametrize(
56+
"before,after,reason",
57+
ORDERING_INVARIANTS,
58+
ids=[
59+
"plan_before_file_write",
60+
"task_message_before_task_retrieval",
61+
"task_message_before_task_completed",
62+
"task_message_before_task_async",
63+
],
64+
)
65+
def test_tool_dispatch_ordering_invariant(
66+
before: Predicate,
67+
after: Predicate,
68+
reason: str,
69+
) -> None:
70+
before_idx = _predicate_index(before)
71+
after_idx = _predicate_index(after)
72+
assert before_idx < after_idx, (
73+
f"_TOOL_RESULT_DISPATCH ordering violation: "
74+
f"{before.__name__} (index {before_idx}) must precede "
75+
f"{after.__name__} (index {after_idx}). Reason: {reason}"
76+
)

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)