97 findings to zero. Most were mechanical — 52 unsorted import blocks, 10 unsorted __all__, assorted pyupgrade and simplify hints. Two were not, and both were visible only because the lint made me look. `webber version` did not exist. src/cli/commands/version.py defines show_version(), main.py imported it, and the registration line was never written — the CLI exposed chat and explore only. The import carried `# noqa: F401`, which is what kept the omission quiet: someone marked the symptom as intentional instead of asking why it was unused. show_version is not redundant with the --version flag; it prints the resolved Ollama URL, model and debug state, which is the form worth having when something is misconfigured. Registered, and the suppression dropped because the import is now genuinely used. test_spawn_explore_agent asserted nothing. It built a mock RunContext, patched get_agent, and stopped at the comment "For now, verify the explore agent would be called correctly". It had been counted as a passing test. An AST sweep of all 238 test functions found it was the only one, which is worth knowing — the problem was contained, not systemic. It is now skipped with a reason, so it reports as unfinished rather than as passing. Reducing it rather than deleting its imports was the point: tidying the imports would have made a hollow test look clean. Two findings were false positives, and both are recorded rather than silently worked around: B023 flagged run_agent closing over full_prompt and ctx. Traced: agent_task is awaited at line 326 before `continue` reaches the next iteration, so neither name can be rebound while the closure is pending, and the exception path cancels and awaits too. Not a bug. Bound as defaults anyway, because that stays true if the await ever moves. I had called it a live bug before tracing it, which is the mistake Rule 5 exists for. RUF012 flagged `rules: list[ApprovalRule] = []` on ApprovalRuleSet. Its suggested fix — annotate ClassVar — would remove the field from the model. ApprovalRuleSet is a pydantic model and pydantic deep-copies defaults per instance; verified by constructing two and confirming their lists are distinct objects. Suppressed with that evidence in the comment. Ruff cannot see the pydantic base because BaseSchema is a local subclass of BaseModel. Also moved a stray `from src.shared.logging import ...` that had drifted below a function definition, and merged a nested if in the ollama provider. 215 passed, 23 skipped, unchanged except for the new skip. `webber version` exercised end to end. mypy is NOT addressed here and the gate still fails on it — 55 errors in 14 files, 35 of them no-any-return from pydantic_ai's untyped returns. That was hidden behind ruff, because the gate stops at the first failing stage. Co-Authored-By: Claude <noreply@anthropic.com>
90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
"""
|
|
Tests for token counting utilities.
|
|
"""
|
|
|
|
from src.shared.tokens import count_message_tokens, count_tokens, estimate_tokens
|
|
|
|
|
|
class TestTokenCounting:
|
|
"""Tests for token counting functions."""
|
|
|
|
def test_estimate_tokens_basic(self):
|
|
"""Test basic token estimation."""
|
|
text = "Hello world"
|
|
tokens = estimate_tokens(text)
|
|
# ~4 chars per token
|
|
assert tokens == len(text) // 4
|
|
|
|
def test_estimate_tokens_empty(self):
|
|
"""Test estimation with empty string."""
|
|
assert estimate_tokens("") == 0
|
|
|
|
def test_estimate_tokens_long_text(self):
|
|
"""Test estimation with longer text."""
|
|
text = "a" * 400
|
|
tokens = estimate_tokens(text)
|
|
assert tokens == 100
|
|
|
|
def test_count_tokens_basic(self):
|
|
"""Test actual token counting."""
|
|
text = "Hello, how are you today?"
|
|
tokens = count_tokens(text)
|
|
# Should return reasonable token count
|
|
assert tokens > 0
|
|
assert tokens < len(text) # Should be fewer tokens than characters
|
|
|
|
def test_count_tokens_empty(self):
|
|
"""Test counting empty string."""
|
|
tokens = count_tokens("")
|
|
assert tokens == 0
|
|
|
|
def test_count_message_tokens_single(self):
|
|
"""Test counting tokens in single message."""
|
|
messages = [{"role": "user", "content": "Hello"}]
|
|
tokens = count_message_tokens(messages)
|
|
assert tokens > 0
|
|
|
|
def test_count_message_tokens_multiple(self):
|
|
"""Test counting tokens in multiple messages."""
|
|
messages = [
|
|
{"role": "user", "content": "Hello, how are you?"},
|
|
{"role": "assistant", "content": "I'm doing well, thank you!"},
|
|
]
|
|
tokens = count_message_tokens(messages)
|
|
# Should be more than single message
|
|
single_tokens = count_message_tokens([messages[0]])
|
|
assert tokens > single_tokens
|
|
|
|
def test_count_message_tokens_empty_list(self):
|
|
"""Test counting empty message list."""
|
|
tokens = count_message_tokens([])
|
|
# tiktoken returns small overhead for empty list (assistant priming)
|
|
assert tokens < 10
|
|
|
|
|
|
class TestTokenCountingAccuracy:
|
|
"""Tests for token counting accuracy."""
|
|
|
|
def test_code_tokens_reasonable(self):
|
|
"""Test that code is tokenized reasonably."""
|
|
code = """
|
|
def hello_world():
|
|
print("Hello, World!")
|
|
return True
|
|
"""
|
|
tokens = count_tokens(code)
|
|
# Code should have reasonable token count
|
|
assert 10 < tokens < 100
|
|
|
|
def test_special_characters(self):
|
|
"""Test tokenization of special characters."""
|
|
text = "Hello! @#$%^&*() World?"
|
|
tokens = count_tokens(text)
|
|
assert tokens > 0
|
|
|
|
def test_unicode_text(self):
|
|
"""Test tokenization of unicode text."""
|
|
text = "Hello 世界 🌍"
|
|
tokens = count_tokens(text)
|
|
assert tokens > 0
|