feat: add self-tdd watchdog — continuous test runner CLI

Adds `src/self_tdd/watchdog.py` with a `_run_tests()` function that
shells out to pytest and a `watch` command that polls on a configurable
interval, printing green on recovery and full short-traceback output on
regression. No files are modified and no commits are made automatically.

Usage:
    self-tdd watch            # default 60s interval
    self-tdd watch -i 15      # poll every 15s

Also adds 6 unit tests and wires the `self-tdd` entry point +
`src/self_tdd` wheel include into pyproject.toml.

https://claude.ai/code/session_01DMjQ5qMZ8iHeyix1j3GS7c
This commit is contained in:
Claude
2026-02-21 16:36:56 +00:00
parent 982c42ba45
commit 7619407b63
4 changed files with 127 additions and 1 deletions

0
src/self_tdd/__init__.py Normal file
View File

71
src/self_tdd/watchdog.py Normal file
View File

@@ -0,0 +1,71 @@
"""Self-TDD Watchdog — polls pytest on a schedule and reports regressions.
Run in a terminal alongside your normal dev work:
self-tdd watch
self-tdd watch --interval 30
The watchdog runs silently while tests pass. When a regression appears it
prints the full short-traceback output so you can see exactly what broke.
No files are modified; no commits are made. Ctrl-C to stop.
"""
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
import typer
# Project root is three levels up from src/self_tdd/watchdog.py
PROJECT_ROOT = Path(__file__).parent.parent.parent
app = typer.Typer(help="Self-TDD watchdog — continuous test runner")
def _run_tests() -> tuple[bool, str]:
"""Run the test suite and return (passed, combined_output)."""
result = subprocess.run(
[sys.executable, "-m", "pytest", "tests/", "-q", "--tb=short"],
capture_output=True,
text=True,
cwd=PROJECT_ROOT,
timeout=60,
)
return result.returncode == 0, (result.stdout + result.stderr).strip()
@app.command()
def watch(
interval: int = typer.Option(60, "--interval", "-i", help="Seconds between test runs"),
) -> None:
"""Poll pytest continuously and print regressions as they appear."""
typer.echo(f"Self-TDD watchdog started — polling every {interval}s. Ctrl-C to stop.")
last_passed: bool | None = None
try:
while True:
passed, output = _run_tests()
stamp = datetime.now().strftime("%H:%M:%S")
if passed:
if last_passed is not True:
typer.secho(f"[{stamp}] All tests passing.", fg=typer.colors.GREEN)
else:
typer.secho(f"[{stamp}] Regression detected:", fg=typer.colors.RED)
typer.echo(output)
last_passed = passed
time.sleep(interval)
except KeyboardInterrupt:
typer.echo("\nWatchdog stopped.")
def main() -> None:
app()
if __name__ == "__main__":
main()