"""Acceptance test for time-to-green task T05 (written for the 2026-10-30 bench; the protocol names it, does not ship it). T05: tally: `head -n 0` must print only the header row (today it prints the header plus nothing, which is right, but there is no test); add the test and keep behaviour. Green: python -m unittest tasks.t05_test (run from the repo root) The behaviour already holds, so "green" has to mean "a test now pins it". This module checks three things: 1. behaviour kept: `head -n 0` prints exactly the header row and exits 0; 2. the repo's own test files (every *test*.py in the repo root, i.e. outside tasks/) still pass; 3. the new test pins the behaviour: the repo's test files are re-run against two mutants of tally in a temporary copy (mutant A: `-n 0` prints the header plus one data row; mutant B: `-n 0` prints nothing), and each mutant must make at least one of them fail. The unmodified repo fails check 3, because no shipped test exercises `-n 0`. """ from __future__ import annotations import glob import importlib.util import io import os import shutil import subprocess import sys import tempfile import unittest from tasks.regress_tally import HeadTests, StatsTests # noqa: F401 (collected as regression tests) ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TOOL = "build-a-cli-tool-with-an-agent-from-a-spec.py" _spec = importlib.util.spec_from_file_location("tally_t05", os.path.join(ROOT, TOOL)) tally = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(tally) GUARD = 'if __name__ == "__main__":' MUTANTS = { "A_header_plus_one_row": " args.n = 1\n", "B_prints_nothing": " return 0\n", } def mutate(source: str, body: str) -> str: """Wrap cmd_head so `-n 0` misbehaves; everything else is untouched.""" wrapper = ("_ttg_original_cmd_head = cmd_head\n\n\n" "def cmd_head(args, out, err):\n" " if getattr(args, 'n', None) == 0:\n" f"{body}" " return _ttg_original_cmd_head(args, out, err)\n\n\n") i = source.rfind(GUARD) return source[:i] + wrapper + source[i:] if i >= 0 else source + "\n\n" + wrapper def repo_test_files() -> list[str]: return sorted(os.path.basename(p) for p in glob.glob(os.path.join(ROOT, "*test*.py"))) def run_repo_tests(where: str) -> list[tuple[str, int, str]]: results = [] for name in repo_test_files(): p = subprocess.run([sys.executable, "-m", "unittest", name], cwd=where, capture_output=True, text=True, timeout=120) results.append((name, p.returncode, (p.stdout + p.stderr)[-800:])) return results class HeadZeroTests(unittest.TestCase): def test_head_n_0_prints_only_the_header_row(self): with tempfile.TemporaryDirectory() as tmp: path = os.path.join(tmp, "data.csv") with open(path, "w", encoding="utf-8", newline="") as fh: fh.write("name,v\nr0,0\nr1,1\n") out, err = io.StringIO(), io.StringIO() code = tally.main(["head", path, "-n", "0"], out=out, err=err) self.assertEqual(code, 0, err.getvalue()) self.assertEqual(out.getvalue(), "name,v\n") def test_repo_test_files_pass(self): files = repo_test_files() self.assertTrue(files, "no *test*.py files in the repo root") for name, code, tail in run_repo_tests(ROOT): with self.subTest(file=name): self.assertEqual(code, 0, tail) def test_repo_tests_catch_a_broken_head_n_0(self): with open(os.path.join(ROOT, TOOL), encoding="utf-8") as fh: source = fh.read() self.assertIn("cmd_head", source, "tally no longer defines cmd_head; mutation check cannot run") for label, body in MUTANTS.items(): with self.subTest(mutant=label), tempfile.TemporaryDirectory() as tmp: for p in glob.glob(os.path.join(ROOT, "*.py")): shutil.copy(p, tmp) with open(os.path.join(tmp, TOOL), "w", encoding="utf-8") as fh: fh.write(mutate(source, body)) results = run_repo_tests(tmp) self.assertTrue(any(code != 0 for _, code, _ in results), f"mutant {label}: every repo test file still passes, so no test pins head -n 0") if __name__ == "__main__": unittest.main()