aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/tests/test_areas.py
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 07:27:17 +0200
committergodosa <godosa@godosa.eu>2026-10-07 07:27:17 +0200
commit81d4e80fd5aabe4e80f58e960affa795cf7d34ec (patch)
treee98eeac2af6af63aa4287bba1f6d4a3af26b5727 /tests/test_areas.py
downloadworkflow-81d4e80fd5aabe4e80f58e960affa795cf7d34ec.tar.gz
workflow-81d4e80fd5aabe4e80f58e960affa795cf7d34ec.zip
workflow: initial public history
Diffstat (limited to 'tests/test_areas.py')
-rw-r--r--tests/test_areas.py319
1 files changed, 319 insertions, 0 deletions
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 "
+ "<Checked>..HEAD -- <paths>; 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()