From 81d4e80fd5aabe4e80f58e960affa795cf7d34ec Mon Sep 17 00:00:00 2001 From: godosa Date: Wed, 7 Oct 2026 07:27:17 +0200 Subject: workflow: initial public history --- tests/test_areas.py | 319 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 tests/test_areas.py (limited to 'tests/test_areas.py') diff --git a/tests/test_areas.py b/tests/test_areas.py new file mode 100644 index 0000000..558230d --- /dev/null +++ b/tests/test_areas.py @@ -0,0 +1,319 @@ +import subprocess +import tempfile +import unittest +from pathlib import Path + +from test_cli import Cli +from test_claims import git +from test_merge import IDENT +from wflib import areas # noqa: E402 (tests/ run with the repo root on sys.path) + +NOTES = """\ +# CLAUDE.md + +## Areas +### Parser +- Code map: `parse_item`, `NOPE_GONE` +- Test recipe: python3 -m unittest tests.test_p +- Paths: src/ +- Checked: {sha} + +### Docs +- Code map: `intro` + +## Gotchas +- x +""" + + +def build(root: Path) -> tuple[str, str]: + """Repo with src/p.py + docs.md, then 3 commits touching src/; returns (sha1, notes).""" + git(root, "init", "-q", "-b", "master") + (root / "src").mkdir(exist_ok=True) + (root / "src" / "p.py").write_text("def parse_item(): pass\n") + (root / "docs.md").write_text("intro\n") + (root / ".gitignore").write_text(".wf/\n") + git(root, "add", "-A") + git(root, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "c1") + sha1 = subprocess.run(["git", "rev-parse", "--short", "HEAD"], cwd=root, capture_output=True, text=True).stdout.strip() + for n in "abc": + (root / "src" / f"{n}.txt").write_text(n) + git(root, "add", "-A") + git(root, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", n) + return sha1, NOTES.format(sha=sha1) + + +class AreasTest(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name).resolve() + self.sha1, self.notes = build(self.root) + + def tearDown(self): + self.tmp.cleanup() + + def test_parse(self): + a = areas.parse(NOTES.format(sha="abc1234")) + self.assertEqual([(x.name, x.slug, x.anchors, x.paths, x.checked) for x in a], [ + ("Parser", "parser", ["parse_item", "NOPE_GONE"], ["src/"], "abc1234"), + ("Docs", "docs", ["intro"], [], None)]) + + def test_missing_and_commits(self): + parser, docs = areas.parse(self.notes) + self.assertEqual(areas.missing(self.root, parser), ["NOPE_GONE"]) + self.assertEqual(areas.commits_since(self.root, parser), 3) + self.assertEqual(areas.missing(self.root, docs), []) + self.assertIsNone(areas.commits_since(self.root, docs)) + + def test_stale(self): + parser, docs = areas.parse(self.notes) + self.assertEqual(areas.status(self.root, parser, 20), (True, ["NOPE_GONE"], 3)) + self.assertEqual(areas.status(self.root, docs, 20), (False, [], None)) + fixed = areas.parse(self.notes.replace(", `NOPE_GONE`", ""))[0] + self.assertEqual(areas.status(self.root, fixed, 3), (True, [], 3)) + self.assertEqual(areas.status(self.root, fixed, 4), (False, [], 3)) + + def test_unknown_checked_sha(self): + a = areas.parse(NOTES.format(sha="deadbee"))[0] + self.assertIsNone(areas.commits_since(self.root, a)) + + def test_mark(self): + out = areas.mark(NOTES.format(sha="abc1234"), "Docs", "f00ba44") + self.assertIn("### Docs\n- Code map: `intro`\n- Checked: f00ba44\n\n## Gotchas", out) + out = areas.mark(out, "Parser", "1111111") + self.assertIn("- Paths: src/\n- Checked: 1111111\n", out) + + +class MatchTest(unittest.TestCase): + def test_matches(self): + parser, docs = areas.parse(NOTES.format(sha="abc1234")) + self.assertTrue(areas.matches(parser, "Fix src/p.py crash")) + self.assertTrue(areas.matches(parser, "see x.py/src/p.py")) + self.assertTrue(areas.matches(parser, "parse_item() loops")) + self.assertTrue(areas.matches(parser, "the parser area")) + self.assertFalse(areas.matches(parser, "parse_items and srcs")) + self.assertFalse(areas.matches(docs, "introduction")) + self.assertTrue(areas.matches(docs, "docs intro")) + self.assertFalse(areas.matches(parser, "Fix src/p.py crash", {"src"})) + + def test_shared_paths(self): + a = areas.parse("## Areas\n### A\n- Paths: x.py lib/\n### B\n- Paths: ./x.py y.py\n### C\n- Paths: lib\n") + self.assertEqual(areas.shared_paths(a), {"x.py", "lib"}) + + def test_block(self): + text = NOTES.format(sha="abc1234") + self.assertEqual(areas.block(text, "Docs"), "### Docs\n- Code map: `intro`") + self.assertEqual(areas.block(text, "Parser").splitlines()[-1], "- Checked: abc1234") + self.assertEqual(areas.block(text, "X"), "") + + +class CodeRootTest(Cli): + """code_root: area anchors / Checked / staleness live in another repo than the project.""" + + def setUp(self): + super().setUp() + self.code = self.root.parent / "code" + self.code.mkdir() + self.sha1, notes = build(self.code) + (self.root / "CLAUDE.md").write_text(notes) + with open(self.root / "workflow.toml", "a") as f: + f.write('code_root = "../code"\n') + git(self.root, "init", "-q", "-b", "master") + (self.root / "engine").mkdir() + (self.root / "engine" / "x.py").write_text("x") + + def test_areas_use_code_root(self): + self.assertEqual(self.ok("areas"), f"Parser: stale · missing: NOPE_GONE · 3 commits since {self.sha1}\n" + "Docs: ok · no Checked\n") + + def test_mark_uses_code_head(self): + self.ok("areas", "--mark", "Docs") + head = subprocess.run(["git", "rev-parse", "--short", "HEAD"], cwd=self.code, capture_output=True, + text=True).stdout.strip() + self.assertIn(f"- Checked: {head}", (self.root / "CLAUDE.md").read_text()) + + def test_check_uses_code_root(self): + out = self.ok("check") + self.assertIn("area Parser: anchor NOPE_GONE not found", out) + self.assertNotIn("Checked", out) + self.assertNotIn("anchor intro", out) + + def test_no_uncovered_nudge(self): + self.assertNotIn("uncovered", self.ok("areas")) + + +class CtxAreaTest(Cli): + tasks_text = ("# T\n\n## Pending\n\n- **t-a** [P1] (<1h): A. Fix src/p.py.\n\n" + "- **t-b** [P1] (<1h): B. Unrelated.\n") + + def setUp(self): + super().setUp() + (self.root / "CLAUDE.md").write_text(NOTES.format(sha="abc1234")) + + def test_ctx_prints_matching_area(self): + out = self.ok("ctx", "t-a") + self.assertIn("\nArea Parser (CLAUDE.md):\n### Parser\n- Code map: `parse_item`, `NOPE_GONE`\n" + "- Test recipe: python3 -m unittest tests.test_p\n", out) + self.assertNotIn("### Docs", out) + + def test_ctx_no_match(self): + self.assertNotIn("Area ", self.ok("ctx", "t-b")) + + def test_ctx_shared_path_no_match(self): + (self.root / "CLAUDE.md").write_text(NOTES.format(sha="abc1234").replace("- Code map: `intro`", + "- Code map: `intro`\n- Paths: src/")) + self.assertNotIn("Area ", self.ok("ctx", "t-a")) + + +class AreasCliTest(Cli): + def setUp(self): + super().setUp() + sha1, notes = build(self.root) + self.sha1 = sha1 + (self.root / "CLAUDE.md").write_text(notes) + + def test_areas_output(self): + out = self.ok("areas") + self.assertEqual(out, f"Parser: stale · missing: NOPE_GONE · 3 commits since {self.sha1}\n" + "Docs: ok · no Checked\n") + + def test_mark_then_show(self): + self.ok("areas", "--mark", "Docs") + head = subprocess.run(["git", "rev-parse", "--short", "HEAD"], cwd=self.root, capture_output=True, + text=True).stdout.strip() + self.assertEqual(self.ok("areas", "Docs"), f"Docs: ok · 0 commits since {head}\n") + + def test_mark_unique_prefix(self): + self.ok("areas", "--mark", "Do") + head = subprocess.run(["git", "rev-parse", "--short", "HEAD"], cwd=self.root, capture_output=True, + text=True).stdout.strip() + self.assertEqual(self.ok("areas", "Doc"), f"Docs: ok · 0 commits since {head}\n") + + def test_ambiguous_prefix(self): + txt = (self.root / "CLAUDE.md").read_text().replace("### Docs", "### Parser2 (x)") + (self.root / "CLAUDE.md").write_text(txt) + err = self.fails("areas", "--mark", "Par", code=2) + self.assertIn("ambiguous area 'Par'", err) + self.assertIn("Parser, Parser2 (x)", err) + self.ok("areas", "--mark", "Parser") + + def test_unknown_area(self): + err = self.fails("areas", "X", code=2) + self.assertIn("wf: no area 'X' in CLAUDE.md (Parser, Docs)", err) + + def test_check_warnings(self): + out = self.ok("check") + self.assertIn("area Parser: anchor NOPE_GONE not found", out) + (self.root / "CLAUDE.md").write_text(NOTES.format(sha="deadbee").replace("### Parser", "### X")) + self.assertIn("area X: Checked deadbee unknown", self.ok("check")) + + +class AutoTaskTest(Cli): + tasks_text = "# T\n\n## Pending\n\n- **t-a** [P1] (<1h): A. Do.\n\n- **t-b** [P1] (<1h): B. Do.\n" + + def setUp(self): + super().setUp() + sha1, notes = build(self.root) + (self.root / "CLAUDE.md").write_text(notes) + + def test_auto_task(self): + out = self.ok("done", "t-a", "-m", "x") + self.assertIn("added t-map-parser (area map stale)", out) + shown = self.ok("show", "t-map-parser") + self.assertIn("- **t-map-parser** [P1] (<1h): Refresh Parser area map.", shown) + self.assertIn("Steps: wf areas Parser; fix missing anchors + test recipe from git log --stat " + "..HEAD -- ; wf areas --mark Parser.", shown) + self.assertIn("Done: wf areas Parser shows ok.", shown) + self.assertIn("Model: sonnet", shown) + + def test_auto_task_once(self): + self.ok("done", "t-a", "-m", "x") + out = self.ok("done", "t-b", "-m", "y") + self.assertNotIn("added", out) + self.assertEqual(self.ok("list").count("t-map-parser"), 1) + + def test_archived_gets_suffix(self): + self.ok("done", "t-a", "-m", "x") + out = self.ok("done", "t-map-parser", "-m", "z") + self.assertIn("added t-map-parser-2 (area map stale)", out) + + def test_no_areas_file(self): + (self.root / "CLAUDE.md").unlink() + self.assertNotIn("added", self.ok("done", "t-a", "-m", "x")) + + +class UncoveredTest(unittest.TestCase): + def test_uncovered_groups_two_levels(self): + a = areas.parse(NOTES.format(sha="abc1234")) + files = ["src/p.py", "lib/x/y/z.py", "lib/x/w.py", "lib/q.py", "README.md", "tests/t.py", + ".github/ci.yml", "TASKS.md"] + self.assertEqual(areas.uncovered(files, a, ["tests"]), [("lib/x", 2), ("lib", 1)]) + + def test_paths_globs_and_files(self): + a = areas.parse("## Areas\n### A\n- Paths: app/*.py tools/run.sh\n") + self.assertEqual(areas.uncovered(["app/m.py", "tools/run.sh", "tools/other.sh"], a, []), [("tools", 1)]) + + def test_no_paths_anywhere_no_nudge(self): + a = areas.parse("## Areas\n### A\n- Code map: `x`\n") + self.assertEqual(areas.uncovered(["lib/q.py"], a, []), []) + + +class AnchorPathsTest(unittest.TestCase): + def test_anchor_folders_stand_in(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) + build(root) + a = areas.parse("## Areas\n### A\n- Code map: `parse_item`, `intro`\n### B\n- Paths: lib/\n") + self.assertEqual(areas.with_anchor_paths(root, a[0]).paths, ["docs.md", "src"]) + self.assertEqual(areas.with_anchor_paths(root, a[1]).paths, ["lib/"]) + + +class UncoveredCliTest(Cli): + tasks_text = "# T\n\n## Pending\n\n- **t-a** [P1] (<1h): A. Do.\n\n- **t-b** [P1] (<1h): B. Do.\n" + + def setUp(self): + super().setUp() + build(self.root) + notes = NOTES.format(sha="x").replace(", `NOPE_GONE`", "").replace("- Checked: x\n", "") + (self.root / "CLAUDE.md").write_text(notes) + git(self.root, "add", "-A") + git(self.root, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "notes") + (self.root / "engine" / "net").mkdir(parents=True) + (self.root / "engine" / "net" / "sock.py").write_text("x") + (self.root / "docs.md").write_text("changed") + + def test_done_adds_map_task(self): + out = self.ok("done", "t-a", "-m", "x") + self.assertIn("added t-map-engine-net (uncovered area: engine/net, 1 file)", out) + shown = self.ok("show", "t-map-engine-net") + self.assertIn("- **t-map-engine-net** [P1] (<1h): Map engine/net area. " + "No area's Paths covers it.", shown) + self.assertIn("Done: wf areas lists the area ok and no longer names engine/net uncovered.", shown) + self.assertIn("Model: sonnet", shown) + self.assertNotIn("added", self.ok("done", "t-b", "-m", "y")) + + def test_areas_names_uncovered(self): + self.assertIn("uncovered: engine/net (1 file)", self.ok("areas")) + + def test_branch_diff_counts(self): + git(self.root, "switch", "-qc", "topic") + git(self.root, "add", "-A") + git(self.root, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "work") + self.assertIn("uncovered: engine/net (1 file)", self.ok("areas")) + + def test_no_paths_uses_anchor_folders(self): + (self.root / "CLAUDE.md").write_text("## Areas\n### P\n- Code map: `parse_item`\n") + (self.root / "src" / "n.py").write_text("x") + out = self.ok("areas") + self.assertIn("uncovered: engine/net (1 file)", out) + self.assertNotIn("uncovered: src", out) + + def test_ignore_config(self): + with open(self.root / "workflow.toml", "a") as f: + f.write('area_ignore = ["engine"]\n') + self.assertNotIn("uncovered", self.ok("areas")) + self.assertNotIn("added", self.ok("done", "t-a", "-m", "x")) + + +if __name__ == "__main__": + unittest.main() -- cgit