cc-os/plugins/os-doc-hygiene/tests/test_token_estimator.py

232 lines
9.1 KiB
Python
Raw Normal View History

"""
Tests for scripts/token_estimator.py
Covers the deterministic token estimator (Phase 2):
- HeuristicEstimator chars/4 math incl. empty + boundary lengths
- default_estimator() returns a working estimator with no deps / no network
- file estimation + graceful decode handling
- schema-shaped report wrapper (raw_tokens only in v1)
- CLI main() JSON output + exit codes
- tiktoken backend (gated by importorskip; relies on vendored cache, never
hits the network)
sys.path is patched here (no shared conftest) so the test file is
self-contained, matching the repo's test style.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
# ---------------------------------------------------------------------------
# Path bootstrap — inject scripts/ so we can import token_estimator directly.
# ---------------------------------------------------------------------------
_SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
from token_estimator import ( # noqa: E402
HeuristicEstimator,
TiktokenEstimator,
TokenEstimator,
default_estimator,
main,
_O200K_BASE_CACHE_FILENAME,
)
# ===========================================================================
# HeuristicEstimator — chars/4 math
# ===========================================================================
class TestHeuristicMath:
def test_empty_string_is_zero(self):
assert HeuristicEstimator().estimate("") == 0
@pytest.mark.parametrize(
"text,expected",
[
("a", 1), # ceil(1/4)
("abc", 1), # ceil(3/4)
("abcd", 1), # boundary: ceil(4/4) == 1
("abcde", 2), # boundary: ceil(5/4) == 2
("abcdefgh", 2), # ceil(8/4) == 2
("a" * 9, 3), # ceil(9/4) == 3
],
)
def test_boundary_lengths(self, text, expected):
assert HeuristicEstimator().estimate(text) == expected
def test_count_is_nonnegative_and_monotone(self):
est = HeuristicEstimator()
prev = -1
for n in range(0, 50):
count = est.estimate("x" * n)
assert count >= 0
assert count >= prev
prev = count
def test_name_is_heuristic(self):
assert HeuristicEstimator().name == "heuristic"
# ===========================================================================
# default_estimator() — always returns a working estimator, no network
# ===========================================================================
class TestDefaultEstimator:
def test_returns_token_estimator_instance(self, tmp_path):
# Empty cache dir => no vendored vocab => must be heuristic, never network.
est = default_estimator(cache_dir=tmp_path)
assert isinstance(est, TokenEstimator)
assert isinstance(est, HeuristicEstimator)
assert est.name == "heuristic"
def test_works_with_no_deps(self, tmp_path):
# The returned estimator must actually estimate, regardless of backend.
est = default_estimator(cache_dir=tmp_path)
assert est.estimate("hello world") > 0
def test_never_raises_on_missing_cache(self, tmp_path):
# Nonexistent cache dir must not raise — falls back cleanly.
missing = tmp_path / "does-not-exist"
est = default_estimator(cache_dir=missing)
assert isinstance(est, HeuristicEstimator)
# ===========================================================================
# File estimation + decode handling
# ===========================================================================
class TestFileEstimation:
def test_estimate_file_counts_contents(self, tmp_path):
f = tmp_path / "doc.md"
f.write_text("abcdefgh", encoding="utf-8") # 8 chars -> 2 tokens
assert HeuristicEstimator().estimate_file(f) == 2
def test_missing_file_yields_zero(self, tmp_path):
assert HeuristicEstimator().estimate_file(tmp_path / "nope.md") == 0
def test_invalid_utf8_decodes_gracefully(self, tmp_path):
f = tmp_path / "binary.md"
# Invalid UTF-8 bytes — must not raise, replacement chars are counted.
f.write_bytes(b"\xff\xfe\xfa\xfbhello")
count = HeuristicEstimator().estimate_file(f)
assert count > 0
def test_empty_file_yields_zero(self, tmp_path):
f = tmp_path / "empty.md"
f.write_text("", encoding="utf-8")
assert HeuristicEstimator().estimate_file(f) == 0
# ===========================================================================
# Schema-shaped report wrapper
# ===========================================================================
class TestReportShape:
def test_estimate_for_report_v1_shape(self):
est = HeuristicEstimator()
out = est.estimate_for_report("abcd") # 1 token
assert out == {
"raw_tokens": 1,
"injection_frequency": None,
"weighted_tokens": None,
}
def test_raw_tokens_matches_estimate(self):
est = HeuristicEstimator()
text = "some longer markdown content here"
assert est.estimate_for_report(text)["raw_tokens"] == est.estimate(text)
# ===========================================================================
# CLI main()
# ===========================================================================
class TestCLI:
def test_main_emits_json_for_file(self, tmp_path, capsys):
f = tmp_path / "doc.md"
f.write_text("abcdefgh", encoding="utf-8")
rc = main([str(f), "--cache-dir", str(tmp_path)])
assert rc == 0
out = json.loads(capsys.readouterr().out)
assert out["path"] == str(f)
assert out["backend"] == "heuristic"
assert out["token_estimate"]["raw_tokens"] == 2
assert out["token_estimate"]["injection_frequency"] is None
def test_main_missing_file_exits_nonzero(self, tmp_path, capsys):
rc = main([str(tmp_path / "missing.md")])
assert rc == 1
err = json.loads(capsys.readouterr().err)
assert "error" in err
# ===========================================================================
# Cache-filename gotcha — guards the offline contract
# ===========================================================================
class TestCacheFilename:
def test_cache_filename_is_sha1_of_blobpath(self):
# If this changes, the vendored-vocab pre-check silently misses and
# tiktoken would fall to the network. Pin it.
assert _O200K_BASE_CACHE_FILENAME == "fb374d419588a4632f3f557e76b4b70aebbca790"
# ===========================================================================
# tiktoken backend — only if importable; must rely on vendored cache, no net.
# ===========================================================================
class TestTiktokenBackend:
def test_tiktoken_estimator_plausible_counts(self):
tiktoken = pytest.importorskip("tiktoken")
# Load directly from the real registry (test env, allowed to read its
# own installed cache; we assert no-network indirectly by using a
# plausible-count check rather than forcing a download).
try:
encoding = tiktoken.get_encoding("o200k_base")
except Exception:
pytest.skip("o200k_base vocab unavailable offline in this env")
est = TiktokenEstimator(encoding, "o200k_base")
assert est.name == "tiktoken:o200k_base"
assert est.estimate("") == 0
# A short English sentence is a handful of tokens, fewer than chars.
text = "The quick brown fox jumps over the lazy dog."
count = est.estimate(text)
assert 0 < count < len(text)
def test_tiktoken_special_tokens_do_not_raise(self):
tiktoken = pytest.importorskip("tiktoken")
try:
encoding = tiktoken.get_encoding("o200k_base")
except Exception:
pytest.skip("o200k_base vocab unavailable offline in this env")
est = TiktokenEstimator(encoding, "o200k_base")
# disallowed_special=() means this special-token-looking text is fine.
assert est.estimate("<|endoftext|> in a doc") > 0
def test_default_estimator_selects_tiktoken_when_cache_present(self, tmp_path):
tiktoken = pytest.importorskip("tiktoken")
# Vendor the vocab into a tmp cache dir under the correct sha1 filename,
# then assert default_estimator picks tiktoken (no network: file is local).
try:
real = tiktoken.get_encoding("o200k_base") # may use installed cache
except Exception:
pytest.skip("cannot load o200k_base offline in this env")
# Best-effort: locate the installed cache file to copy it.
src_dir = Path(
__import__("os").environ.get("TIKTOKEN_CACHE_DIR", "")
)
src = src_dir / _O200K_BASE_CACHE_FILENAME
if not src.is_file():
pytest.skip("vendored vocab file not locatable to copy")
(tmp_path / _O200K_BASE_CACHE_FILENAME).write_bytes(src.read_bytes())
est = default_estimator(cache_dir=tmp_path)
assert est.name == "tiktoken:o200k_base"
assert real.encode("hi", disallowed_special=()) is not None