testsAndMisc/python_pkg/code_tutor/tests/test_progress.py
Krzysztof kuhy Rudnicki 2d0d69b461 Add code_tutor: interactive codebase-learning CLI
New python_pkg/code_tutor package that turns an unfamiliar codebase into
an interactive lesson: analyse code into items (_analyzer), order them
dependency-first (_plan_builder), pose reimplement challenges
(_challenge + _challenge_support), verify user implementations with tests
and coverage (_verifier), track progress (_progress/_session), and offer
local-LLM hints (_llm), driven by a Typer/Rich CLI (cli).

- meta/pyproject.toml + .pre-commit-config.yaml + meta/requirements.txt:
  add anthropic/rich/typer deps, a cli.py per-file-ignore (Typer B008 etc),
  and code_tutor-specific ruff/pylint/mypy tuning.
- _challenge.py and its tests are split across _challenge_support.py and
  test_challenge_part2/part3.py to stay under the 500-line file cap.

Verification: pytest 631 passed at 100% line+branch coverage; every file
<=500 lines; pre-commit (ruff ALL, mypy strict, pylint 10/10, bandit,
evidence + multi-file contract) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
2026-07-11 00:15:46 +02:00

174 lines
5.0 KiB
Python

"""Tests for python_pkg.code_tutor._progress."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from python_pkg.code_tutor._progress import (
LessonRecord,
append_session_record,
config_dir,
item_from_data,
load_plan,
load_progress,
save_plan,
save_progress,
)
def _fake_home(tmp_path: Path) -> Path:
home = tmp_path / "home"
home.mkdir()
return home
# ---------------------------------------------------------------------------
# config_dir
# ---------------------------------------------------------------------------
def test_config_dir_returns_path(tmp_path: Path) -> None:
with patch(
"python_pkg.code_tutor._progress.Path.home", return_value=_fake_home(tmp_path)
):
result = config_dir(tmp_path / "codebase")
assert isinstance(result, Path)
assert "code_tutor" in str(result)
# ---------------------------------------------------------------------------
# load_plan / save_plan
# ---------------------------------------------------------------------------
def test_load_plan_missing(tmp_path: Path) -> None:
with patch(
"python_pkg.code_tutor._progress.Path.home", return_value=_fake_home(tmp_path)
):
result = load_plan(tmp_path / "cb")
assert result is None
def test_save_and_load_plan(tmp_path: Path) -> None:
home = _fake_home(tmp_path)
codebase = tmp_path / "cb"
plan: dict[str, object] = {
"codebase_path": str(codebase),
"created_at": "2026-01-01T00:00:00+00:00",
"total_items": 0,
"source_fingerprint": "abc",
"sessions": [],
}
with patch("python_pkg.code_tutor._progress.Path.home", return_value=home):
save_plan(codebase, plan)
loaded = load_plan(codebase)
assert loaded is not None
assert loaded["total_items"] == 0
# ---------------------------------------------------------------------------
# load_progress / save_progress
# ---------------------------------------------------------------------------
def test_load_progress_missing(tmp_path: Path) -> None:
with patch(
"python_pkg.code_tutor._progress.Path.home", return_value=_fake_home(tmp_path)
):
result = load_progress(tmp_path / "cb")
assert result["learned"] == []
assert result["struggled"] == []
assert result["skipped"] == []
assert result["last_session"] == ""
def test_save_and_load_progress(tmp_path: Path) -> None:
home = _fake_home(tmp_path)
codebase = tmp_path / "cb"
progress = {
"learned": ["a.fn"],
"struggled": [],
"skipped": ["b.fn"],
"last_session": "2026-01-01T00:00:00+00:00",
}
with patch("python_pkg.code_tutor._progress.Path.home", return_value=home):
save_progress(codebase, progress)
loaded = load_progress(codebase)
assert loaded["learned"] == ["a.fn"]
assert loaded["skipped"] == ["b.fn"]
# ---------------------------------------------------------------------------
# append_session_record
# ---------------------------------------------------------------------------
def test_append_session_record(tmp_path: Path) -> None:
home = _fake_home(tmp_path)
codebase = tmp_path / "cb"
record = LessonRecord(
timestamp="2026-01-01T00:00:00+00:00",
item_id="mod.fn",
file="mod.py",
lines="1-10",
snippet="def fn(): pass",
outcome="learned",
answers={"Purpose": "does stuff"},
improvement="",
verdict="PASS",
attempt=1,
)
with patch("python_pkg.code_tutor._progress.Path.home", return_value=home):
append_session_record(codebase, record)
cfg = config_dir(codebase)
sessions_dir = cfg / "sessions"
assert sessions_dir.exists()
jsonl_files = list(sessions_dir.glob("*.jsonl"))
assert len(jsonl_files) == 1
content = jsonl_files[0].read_text(encoding="utf-8")
assert "mod.fn" in content
# ---------------------------------------------------------------------------
# item_from_data
# ---------------------------------------------------------------------------
def test_item_from_data() -> None:
data = {
"id": "mod.fn",
"file": "mod.py",
"type": "function",
"name": "fn",
"start_line": 1,
"end_line": 5,
"class_name": "",
"depends_on": [],
}
item = item_from_data(data)
assert item.id == "mod.fn"
assert item.name == "fn"
assert item.start_line == 1
# ---------------------------------------------------------------------------
# LessonRecord default challenge_result
# ---------------------------------------------------------------------------
def test_lesson_record_defaults() -> None:
record = LessonRecord(
timestamp="t",
item_id="i",
file="f.py",
lines="1-2",
snippet="code",
outcome="learned",
answers={},
improvement="",
verdict="PASS",
attempt=1,
)
assert record.challenge_result == ""