128 lines
5.0 KiB
Python
128 lines
5.0 KiB
Python
"""
|
|
Tests for the scanner's file_length signal (Forgejo issue #25, Part 1).
|
|
|
|
Objective fact only: line count + estimated token count vs configurable
|
|
thresholds. No classification, no report-owned fields (token_estimate is
|
|
owned by report_builder.py, not the scanner).
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
|
|
if str(_SCRIPTS_DIR) not in sys.path:
|
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
|
|
from scanner import Scanner, SignalComputer # noqa: E402
|
|
from token_estimator import TokenEstimator # noqa: E402
|
|
|
|
from test_scanner_signals import ( # noqa: E402
|
|
FORBIDDEN_SIGNAL_FIELDS,
|
|
_assert_no_classifier_fields,
|
|
_assert_signal_present,
|
|
_make_tree,
|
|
)
|
|
|
|
|
|
class _StubEstimator(TokenEstimator):
|
|
"""Deterministic stub: 1 token per line, ignoring content."""
|
|
|
|
name = "stub-per-line"
|
|
|
|
def estimate(self, text: str) -> int:
|
|
if not text:
|
|
return 0
|
|
return len(text.splitlines())
|
|
|
|
|
|
def _run(root: Path, **kwargs) -> dict:
|
|
return Scanner(
|
|
root=root,
|
|
git_log_fn=lambda p: [],
|
|
now_fn=lambda: 0.0,
|
|
token_estimator=kwargs.pop("token_estimator", _StubEstimator()),
|
|
**kwargs,
|
|
).run()
|
|
|
|
|
|
class TestFileLengthSignal:
|
|
def test_short_file_no_signal(self, tmp_path):
|
|
_make_tree(tmp_path, {"doc.md": "\n".join(f"line {i}" for i in range(10))})
|
|
result = _run(tmp_path)
|
|
sigs = result["signals"].get("doc.md", [])
|
|
names = [s["name"] for s in sigs]
|
|
assert "file_length" not in names
|
|
|
|
def test_over_line_threshold_emits_signal(self, tmp_path):
|
|
content = "\n".join(f"line {i}" for i in range(500))
|
|
_make_tree(tmp_path, {"doc.md": content})
|
|
result = _run(tmp_path, max_lines=400, max_tokens=4000)
|
|
sigs = result["signals"].get("doc.md", [])
|
|
sig = _assert_signal_present(sigs, "file_length")
|
|
_assert_no_classifier_fields(sig)
|
|
assert "500" in sig["detail"]
|
|
assert "400" in sig["detail"]
|
|
|
|
def test_over_token_threshold_emits_signal(self, tmp_path):
|
|
"""Line count under threshold but stub estimator pushes tokens over."""
|
|
content = "\n".join(f"line {i}" for i in range(50))
|
|
_make_tree(tmp_path, {"doc.md": content})
|
|
# Stub estimates 1 token/line -> 50 tokens; set max_tokens low so it fires
|
|
# even though line count (50) stays well under max_lines (400).
|
|
result = _run(tmp_path, max_lines=400, max_tokens=10)
|
|
sigs = result["signals"].get("doc.md", [])
|
|
sig = _assert_signal_present(sigs, "file_length")
|
|
_assert_no_classifier_fields(sig)
|
|
assert "threshold 400 lines / 10 tokens" in sig["detail"]
|
|
|
|
def test_configurable_threshold_respected(self, tmp_path):
|
|
content = "\n".join(f"line {i}" for i in range(50))
|
|
_make_tree(tmp_path, {"doc.md": content})
|
|
# Below default threshold -> no signal
|
|
result_default = _run(tmp_path)
|
|
assert "file_length" not in [
|
|
s["name"] for s in result_default["signals"].get("doc.md", [])
|
|
]
|
|
# Lower the threshold below the actual line count -> signal fires
|
|
result_custom = _run(tmp_path, max_lines=10, max_tokens=4000)
|
|
sigs = result_custom["signals"].get("doc.md", [])
|
|
sig = _assert_signal_present(sigs, "file_length")
|
|
assert "threshold 10 lines" in sig["detail"]
|
|
|
|
def test_silent_below_threshold_with_default_estimator(self, tmp_path):
|
|
"""Uses the real default_estimator() (no injected stub) below threshold."""
|
|
_make_tree(tmp_path, {"doc.md": "short content"})
|
|
result = Scanner(
|
|
root=tmp_path,
|
|
git_log_fn=lambda p: [],
|
|
now_fn=lambda: 0.0,
|
|
).run()
|
|
sigs = result["signals"].get("doc.md", [])
|
|
names = [s["name"] for s in sigs]
|
|
assert "file_length" not in names
|
|
|
|
def test_signal_omits_forbidden_report_fields(self, tmp_path):
|
|
"""file_length must not leak report-owned fields (e.g. token_estimate)."""
|
|
content = "\n".join(f"line {i}" for i in range(500))
|
|
_make_tree(tmp_path, {"doc.md": content})
|
|
result = _run(tmp_path, max_lines=400, max_tokens=4000)
|
|
sigs = result["signals"].get("doc.md", [])
|
|
sig = _assert_signal_present(sigs, "file_length")
|
|
for field in FORBIDDEN_SIGNAL_FIELDS:
|
|
assert field not in sig, f"forbidden field '{field}' found in file_length signal"
|
|
|
|
def test_signal_computer_direct_default_thresholds(self, tmp_path):
|
|
"""SignalComputer alone: constructor defaults are 400 lines / 4000 tokens."""
|
|
content = "\n".join(f"line {i}" for i in range(500))
|
|
_make_tree(tmp_path, {"doc.md": content})
|
|
path = tmp_path / "doc.md"
|
|
computer = SignalComputer(
|
|
root=tmp_path,
|
|
git_log_fn=None,
|
|
now_fn=lambda: 0.0,
|
|
token_estimator=_StubEstimator(),
|
|
)
|
|
sigs = computer._file_length(path)
|
|
sig = _assert_signal_present(sigs, "file_length")
|
|
assert "threshold 400 lines / 4000 tokens" in sig["detail"]
|