-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtest_tool_dispatch_sync.py
More file actions
151 lines (124 loc) · 5.45 KB
/
Copy pathtest_tool_dispatch_sync.py
File metadata and controls
151 lines (124 loc) · 5.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Contract test: ``KNOWN_TOOL_TYPES`` must match all four dispatch sites.
Sites (each compared to ``KNOWN_TOOL_TYPES`` in ``utils/tool_dispatch.py``):
- ``static/tool_types.json`` — generated manifest
- ``utils/md_exporter.py`` — ``_render_tool_use`` if/elif branches (parsed)
- ``models/tool_results.py`` — ``ToolNameLiteral``
- ``static/js/render/registry.js`` — ``TOOL_USE_RENDERERS`` keys (parsed)
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import get_args
import pytest
from models.tool_results import ToolNameLiteral
from scripts.gen_tool_types_manifest import write_tool_types_manifest
from utils.tool_dispatch import KNOWN_TOOL_TYPES
_REPO_ROOT = Path(__file__).resolve().parents[1]
_FRONTEND_REGISTRY = _REPO_ROOT / "static" / "js" / "render" / "registry.js"
_MD_EXPORTER = _REPO_ROOT / "utils" / "md_exporter.py"
_TOOL_TYPES_MANIFEST = _REPO_ROOT / "static" / "tool_types.json"
def _format_set_diff(expected: frozenset[str], actual: frozenset[str], site: str) -> str:
missing = sorted(expected - actual)
extra = sorted(actual - expected)
parts: list[str] = []
if missing:
parts.append(f"missing tool type(s) {missing!r} in {site}")
if extra:
parts.append(f"unexpected tool type(s) {extra!r} in {site}")
return "; ".join(parts)
def _parse_frontend_tool_use_renderers(path: Path) -> frozenset[str]:
"""Extract ``TOOL_USE_RENDERERS`` keys.
Assumes values are bare identifiers (``Bash: renderBashUse``). Brace-depth
parsing avoids truncating the object body if a value ever contains ``}``.
"""
text = path.read_text(encoding="utf-8")
marker = "export const TOOL_USE_RENDERERS = {"
start = text.find(marker)
if start == -1:
msg = f"Could not find TOOL_USE_RENDERERS in {path}"
raise ValueError(msg)
i = start + len(marker)
depth = 1
body_start = i
while i < len(text) and depth > 0:
ch = text[i]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
i += 1
if depth != 0:
msg = f"Unbalanced braces in TOOL_USE_RENDERERS in {path}"
raise ValueError(msg)
body = text[body_start : i - 1]
keys = re.findall(r"^\s*(\w+)\s*:", body, re.MULTILINE)
return frozenset(keys)
def _parse_md_exporter_tool_use_handlers(path: Path) -> frozenset[str]:
"""Extract tool names handled by ``_render_tool_use`` if/elif branches."""
text = path.read_text(encoding="utf-8")
match = re.search(
r"def _render_tool_use\(.*?(?=\ndef _render_tool_result)",
text,
re.DOTALL,
)
if not match:
msg = f"Could not find _render_tool_use in {path}"
raise ValueError(msg)
body = match.group(0)
names = set(re.findall(r'(?:if|elif) name == "([^"]+)"', body))
for tuple_match in re.finditer(r"elif name in \(([^)]+)\)", body):
names.update(re.findall(r'"([^"]+)"', tuple_match.group(1)))
return frozenset(names)
def _load_manifest_tool_types(path: Path) -> frozenset[str]:
if not path.is_file():
msg = f"Missing manifest: {path} (run python scripts/gen_tool_types_manifest.py)"
raise ValueError(msg)
data = json.loads(path.read_text(encoding="utf-8"))
raw = data.get("tool_types")
if not isinstance(raw, list):
msg = f"Invalid tool_types in {path}: expected a JSON array"
raise ValueError(msg)
for i, item in enumerate(raw):
if not isinstance(item, str):
msg = f"Invalid tool_types[{i}] in {path}: expected string, got {type(item).__name__}"
raise ValueError(msg)
return frozenset(raw)
def test_tool_types_manifest_matches_known_tool_types() -> None:
site = "static/tool_types.json"
try:
actual = _load_manifest_tool_types(_TOOL_TYPES_MANIFEST)
except ValueError as exc:
pytest.fail(f"{site}: {exc}")
if actual != KNOWN_TOOL_TYPES:
pytest.fail(_format_set_diff(KNOWN_TOOL_TYPES, actual, site))
def test_tool_types_manifest_is_committed_and_current(tmp_path: Path) -> None:
"""Regenerating the manifest must match the committed file."""
expected = tmp_path / "tool_types.json"
write_tool_types_manifest(expected)
committed = _TOOL_TYPES_MANIFEST.read_text(encoding="utf-8")
assert expected.read_text(encoding="utf-8") == committed
def test_md_exporter_handlers_match_known_tool_types() -> None:
site = "utils/md_exporter.py (_render_tool_use branches)"
try:
actual = _parse_md_exporter_tool_use_handlers(_MD_EXPORTER)
except ValueError as exc:
pytest.fail(f"{site}: {exc}")
if actual != KNOWN_TOOL_TYPES:
pytest.fail(_format_set_diff(KNOWN_TOOL_TYPES, actual, site))
def test_tool_name_literal_matches_known_tool_types() -> None:
site = "models/tool_results.py (ToolNameLiteral)"
actual = frozenset(get_args(ToolNameLiteral))
if actual != KNOWN_TOOL_TYPES:
pytest.fail(_format_set_diff(KNOWN_TOOL_TYPES, actual, site))
def test_frontend_registry_matches_known_tool_types() -> None:
"""``TOOL_USE_RENDERERS`` keys must match ``KNOWN_TOOL_TYPES``."""
site = "static/js/render/registry.js (TOOL_USE_RENDERERS)"
try:
actual = _parse_frontend_tool_use_renderers(_FRONTEND_REGISTRY)
except ValueError as exc:
pytest.fail(f"{site}: {exc}")
if actual != KNOWN_TOOL_TYPES:
pytest.fail(_format_set_diff(KNOWN_TOOL_TYPES, actual, site))
def test_known_tool_types_nonempty() -> None:
assert KNOWN_TOOL_TYPES