""" 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"]