diff --git a/docs/superpowers/evidence/sql-drills-harness-2026-11-01.json b/docs/superpowers/evidence/sql-drills-harness-2026-11-01.json index b68b0fed..1d851611 100644 --- a/docs/superpowers/evidence/sql-drills-harness-2026-11-01.json +++ b/docs/superpowers/evidence/sql-drills-harness-2026-11-01.json @@ -3,12 +3,14 @@ "scope": [ "python_pkg/sql_drills (new package, fixtures, tests, README)", "meta/pyproject.toml: register the docker pytest marker", - "Non-goal: bumping the pinned mysql image; copying third-party problems" + "Non-goal: bumping the pinned mysql image; copying third-party problems", + "2026-11-02: show/check send an optional focus-tracker heartbeat (__main__.py, tests/conftest.py, tests/test_focus_hook.py)" ], "changes": [ "CLI start/stop/list/show/check over one long-lived tmpfs container", "Judge-style normaliser and line diff, unit-tested without Docker", - "12 original drills with live-verified reference solutions kept in tests only" + "12 original drills with live-verified reference solutions kept in tests only", + "_heartbeat(): soft import of focus_project; absent -> no-op, tracker error -> logged, result and exit code unchanged" ], "verification": [ { @@ -20,11 +22,17 @@ "command": "python3 -m python_pkg.sql_drills check for all 12 drills", "result": "pass", "evidence": "every drill prints PASS; wrong query prints a line diff, syntax error exits 2" + }, + { + "command": "python3 -m pytest -q python_pkg/sql_drills/tests --cov=python_pkg.sql_drills --cov-branch", + "result": "pass", + "evidence": "61 passed, 15 live tests skipped (no container), 100% branch coverage; show 01 wrote one sql_drills heartbeat row to a scratch FOCUS_PROJECT_DATA" } ], "risks": [ "Local MySQL image must be pulled once; start fails with a clear error otherwise", - "A runaway query is cut off client-side after 30 s but may keep running in the container until stop" + "A runaway query is cut off client-side after 30 s but may keep running in the container until stop", + "A hanging tracker would delay show/check; the real one is a local file append" ], "rollback": [ "git revert the commit; run python3 -m python_pkg.sql_drills stop first", diff --git a/python_pkg/sql_drills/__main__.py b/python_pkg/sql_drills/__main__.py index 21184cbf..77b2a90a 100644 --- a/python_pkg/sql_drills/__main__.py +++ b/python_pkg/sql_drills/__main__.py @@ -3,12 +3,32 @@ from __future__ import annotations import argparse +import importlib +import logging from pathlib import Path import sys from python_pkg.sql_drills import _check, _drills, _engine _EXIT_CODES = {"pass": 0, "fail": 1, "error": 2} +_LOG = logging.getLogger(__name__) + + +def _heartbeat() -> None: + """Tell the optional focus tracker that practice is happening. + + Soft dependency: absent module -> silent no-op; a tracker error is logged + (file or registry errors) and never changes the drill's result or + exit code. + """ + try: + tracker = importlib.import_module("focus_project") + except ImportError: + return + try: + tracker.record_activity("sql_drills", "heartbeat") + except OSError, ValueError: + _LOG.exception("focus tracker heartbeat failed") def _out(line: str) -> None: @@ -44,8 +64,10 @@ def _dispatch(args: argparse.Namespace) -> int: for drill in _drills.list_drills(): _out(f"{drill.id} {_drills.title_of(drill)}") elif args.cmd == "show": + _heartbeat() sys.stdout.write(_drills.render_show(_drills.get_drill(args.drill))) else: + _heartbeat() result = _check.run_check(_drills.get_drill(args.drill), args.sql_file) _out(result.message) return _EXIT_CODES[result.status] diff --git a/python_pkg/sql_drills/tests/conftest.py b/python_pkg/sql_drills/tests/conftest.py new file mode 100644 index 00000000..a02b73e9 --- /dev/null +++ b/python_pkg/sql_drills/tests/conftest.py @@ -0,0 +1,14 @@ +"""Keep the optional focus tracker out of every test unless a test fakes it.""" + +from __future__ import annotations + +import sys +from typing import Any, cast + +import pytest + + +@pytest.fixture(autouse=True) +def _no_real_focus_tracker(monkeypatch: pytest.MonkeyPatch) -> None: + """Make ``import focus_project`` fail, so no test writes live activity.""" + monkeypatch.setitem(sys.modules, "focus_project", cast("Any", None)) diff --git a/python_pkg/sql_drills/tests/test_focus_hook.py b/python_pkg/sql_drills/tests/test_focus_hook.py new file mode 100644 index 00000000..7aa22bd7 --- /dev/null +++ b/python_pkg/sql_drills/tests/test_focus_hook.py @@ -0,0 +1,71 @@ +"""The soft-optional focus-tracker heartbeat on ``show`` and ``check``.""" + +from __future__ import annotations + +import logging +import sys +import types +from typing import TYPE_CHECKING, Any, cast +from unittest.mock import patch + +import pytest + +from python_pkg.sql_drills import __main__ as cli +from python_pkg.sql_drills import _check + +if TYPE_CHECKING: + from pathlib import Path + + +def _fake_tracker( + monkeypatch: pytest.MonkeyPatch, *, fail: bool = False +) -> list[tuple[Any, ...]]: + calls: list[tuple[Any, ...]] = [] + module = cast("Any", types.ModuleType("focus_project")) + + def record_activity(*args: str) -> None: + if fail: + msg = "tracker down" + raise OSError(msg) + calls.append(args) + + module.record_activity = record_activity + monkeypatch.setitem(sys.modules, "focus_project", module) + return calls + + +def test_show_sends_heartbeat(monkeypatch: pytest.MonkeyPatch) -> None: + calls = _fake_tracker(monkeypatch) + assert cli.main(["show", "01"]) == 0 + assert calls == [("sql_drills", "heartbeat")] + + +def test_check_sends_heartbeat(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + calls = _fake_tracker(monkeypatch) + result = _check.CheckResult("fail", "MSG") + with patch.object(_check, "run_check", return_value=result): + assert cli.main(["check", "01", str(tmp_path / "q.sql")]) == 1 + assert calls == [("sql_drills", "heartbeat")] + + +def test_list_sends_no_heartbeat(monkeypatch: pytest.MonkeyPatch) -> None: + calls = _fake_tracker(monkeypatch) + assert cli.main(["list"]) == 0 + assert calls == [] + + +def test_missing_tracker_changes_nothing(capsys: pytest.CaptureFixture[str]) -> None: + assert cli.main(["show", "01"]) == 0 + assert "CREATE TABLE" in capsys.readouterr().out + + +def test_failing_tracker_is_logged_not_raised( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + capsys: pytest.CaptureFixture[str], +) -> None: + _fake_tracker(monkeypatch, fail=True) + with caplog.at_level(logging.ERROR): + assert cli.main(["show", "01"]) == 0 + assert "heartbeat" in caplog.text + assert "CREATE TABLE" in capsys.readouterr().out