diff options
Diffstat (limited to 'tests/test_finish.py')
| -rw-r--r-- | tests/test_finish.py | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/tests/test_finish.py b/tests/test_finish.py new file mode 100644 index 0000000..38fceb1 --- /dev/null +++ b/tests/test_finish.py @@ -0,0 +1,210 @@ +import subprocess +import unittest + +from test_cli import TOML, Cli +from test_claims import git +from test_merge import IDENT + + +class FinishTest(Cli): + def setUp(self): + super().setUp() + git(self.root, "init", "-q", "-b", "master") + (self.root / ".gitignore").write_text(".worktrees/\n.wf/\n") + git(self.root, "add", "-A") + git(self.root, "commit", "-qm", "init") + self.wt = self.root / ".worktrees" / "sonnet" + git(self.root, "worktree", "add", "-q", str(self.wt), "-b", "sonnet/t-three") + + def out(self, *args, cwd=None): + return subprocess.run(["git", *args], cwd=cwd or self.root, capture_output=True, text=True).stdout + + def finish(self, *args, cwd=None): + return self.wf("finish", *args, project=False, cwd=cwd or self.wt, env=IDENT) + + def write_code(self): + (self.wt / "code.txt").write_text("x\n") + + def test_report_line_tool_commit(self): + self.write_code() + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt", "--no-push", + "--tool-commit", "abc1234") + self.assertEqual(code, 0, err) + self.assertRegex(out, r"report: commit [0-9a-f]+ tool abc1234\n$") + + def test_report_line_no_worktree(self): + (self.root / "code.txt").write_text("x\n") + code, out, err = self.wf("finish", "t-three", "-m", "ok", "--commit", "impl", "code.txt", "--no-push", + project=False, cwd=self.root, env=IDENT) + self.assertEqual(code, 0, err) + sha = self.out("rev-parse", "--short", "HEAD").strip() + self.assertTrue(out.endswith(f"report: commit {sha}\n"), out) + + def test_commit_done_merge_in_one(self): + self.write_code() + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt", "--no-push") + self.assertEqual((code, err), (0, ""), out) + self.assertIn("done: t-three → tasks/archive.md\n", out) + self.assertIn("committed code.txt\n", out) + self.assertIn("merged sonnet/t-three into master\n", out) + sha = self.out("log", "--format=%h", "-n1", "--grep=^impl$", "master").strip() + self.assertTrue(out.endswith(f"report: commit {sha}\n"), out) + self.assertNotIn("wf merge", out) # no merge-step reminder: finish merged + self.assertEqual(self.out("log", "--format=%s", "master"), "t-three done\nimpl\ninit\n") + self.assertEqual(self.out("status", "--porcelain"), "") + self.assertEqual((self.root / "code.txt").read_text(), "x\n") + self.assertIn("t-three", (self.root / "tasks" / "archive.md").read_text()) + + def test_wip_commits_notes_clears(self): + self.write_code() + self.wf("status", "t-three", "progress", "br", project=False, cwd=self.wt, env=IDENT) + code, out, err = self.wf("wip", "t-three", "-m", "half done, next: tests", "--commit", "wip", "code.txt", + project=False, cwd=self.wt, env=IDENT) + self.assertEqual(code, 0, err) + self.assertIn("committed code.txt", out) + self.assertEqual(self.out("log", "--format=%s", "-n1", cwd=self.wt), "wip\n") + self.assertEqual(self.out("status", "--porcelain", cwd=self.wt, ).count("code.txt"), 0) + code, out, err = self.wf("show", "t-three", project=False, cwd=self.wt) + self.assertIn("half done, next: tests", out) + self.assertNotIn("in progress", out) + + def test_no_commit_bookkeeping_only(self): + git(self.wt, "switch", "-q", "--detach", "master") + code, out, err = self.finish("t-three", "-m", "ok", "--no-push") + self.assertEqual((code, err), (0, ""), out) + self.assertTrue(out.endswith("committed TASKS.md tasks/archive.md\n"), out) + self.assertEqual(self.out("log", "--format=%s", "master"), "bookkeeping\ninit\n") + + def test_commit_tasks_only_no_path_lists_once(self): + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "bk", "--no-push") + self.assertEqual((code, err), (0, ""), out) + self.assertEqual(out.count("TASKS.md"), 1, out) + self.assertIn("committed TASKS.md tasks/archive.md\n", out) + self.assertEqual(self.out("log", "--format=%s", "master"), "t-three done\ninit\n") + + def test_commit_tasks_path_named_lists_once(self): + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "bk", "TASKS.md", "--no-push", cwd=self.root) + self.assertEqual((code, err), (0, ""), out) + self.assertEqual(out.count("TASKS.md"), 1, out) + self.assertEqual(self.out("log", "--format=%s", "master"), "bk\ninit\n") + + def test_dirty_outside_paths_refused_before_done(self): + self.write_code() + (self.wt / "DESIGN.md").write_text("stray\n") + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt", "--no-push") + self.assertEqual((code, err), (1, "wf: uncommitted changes outside the --commit paths: DESIGN.md\n")) + self.assertIn("t-three", (self.root / "TASKS.md").read_text()) + self.assertEqual(self.out("log", "--format=%s", "master"), "init\n") + + def test_gate_red_refused_before_done(self): + (self.root / "workflow.toml").write_text(TOML + 'quick_gate = ["exit 3"]\n') + git(self.root, "commit", "-qam", "gate") + git(self.wt, "rebase", "-q", "master") + code, out, err = self.finish("t-three", "-m", "ok", "--no-push") + self.assertEqual(code, 1, out + err) + self.assertIn("quick_gate 'exit 3' red", err) + self.assertIn("wf add -p 0", err) + self.assertIn("done+gate-red", err) + self.assertIn("t-three", (self.root / "TASKS.md").read_text()) + + def test_paths_need_commit_message(self): + code, out, err = self.finish("t-three", "-m", "ok", "code.txt") + self.assertEqual(code, 2, out + err) + self.assertIn("paths need --commit", err) + + def test_main_tree_commits_code_and_bookkeeping(self): + (self.root / "code.txt").write_text("y\n") + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt", cwd=self.root) + self.assertEqual((code, err), (0, ""), out) + self.assertEqual(self.out("log", "--format=%s", "master"), "impl\ninit\n") + self.assertEqual(self.out("show", "--stat", "--format=", "master").split("|")[0].strip(), "TASKS.md") + self.assertEqual(self.out("status", "--porcelain"), "") + + def test_main_tree_pushes_home(self): + bare = self.root.parent / "home-finish.git" + subprocess.run(["git", "init", "-q", "--bare", str(bare)], check=True) + git(self.root, "remote", "add", "home", str(bare)) + (self.root / "code.txt").write_text("y\n") + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt", cwd=self.root) + self.assertEqual((code, err), (0, ""), out) + self.assertIn("pushed home\n", out) + self.assertEqual(self.out("log", "--format=%s", "master", cwd=bare), "impl\ninit\n") + + def test_main_tree_no_push_flag(self): + bare = self.root.parent / "home-finish2.git" + subprocess.run(["git", "init", "-q", "--bare", str(bare)], check=True) + git(self.root, "remote", "add", "home", str(bare)) + (self.root / "code.txt").write_text("y\n") + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt", "--no-push", cwd=self.root) + self.assertEqual((code, err), (0, ""), out) + self.assertNotIn("pushed home", out) + + # books stay with the task: no half-done finish, no main-tree done of a worktree's task + + def test_path_outside_repo_refused_before_done(self): + other = self.root.parent / "other-repo-file.md" + other.write_text("x\n") + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", str(other), "--no-push") + self.assertEqual(code, 1, out + err) + self.assertIn("is outside this repo", err) + self.assertIn("t-three", (self.root / "TASKS.md").read_text()) + self.assertEqual(self.out("status", "--porcelain"), "") + + def test_missing_path_refused_before_done(self): + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "shared/CLAUDE.md", "--no-push") + self.assertEqual(code, 1, out + err) + self.assertIn("does not exist", err) + self.assertIn("t-three", (self.root / "TASKS.md").read_text()) + + def test_unchanged_paths_refused_before_done(self): + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "workflow.toml", "--no-push") + self.assertEqual(code, 1, out + err) + self.assertIn("nothing to commit", err) + self.assertIn("t-three", (self.root / "TASKS.md").read_text()) + + def test_worktree_books_copy_refused(self): + (self.wt / "TASKS.md").write_text((self.wt / "TASKS.md").read_text() + "\n") + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "bk", "TASKS.md", "--no-push") + self.assertEqual(code, 1, out + err) + self.assertIn("copy of the books", err) + self.assertIn("t-three", (self.root / "TASKS.md").read_text()) + + def test_already_done_resumes_commit_and_merge(self): + self.write_code() + code, out, err = self.wf("done", "t-three", "-m", "ok", project=False, cwd=self.wt, env=IDENT) + self.assertEqual(code, 0, err) + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt", "--no-push") + self.assertEqual((code, err), (0, ""), out) + self.assertIn("t-three already done (tasks/archive.md): resuming", out) + self.assertEqual(self.out("log", "--format=%s", "master"), "t-three done\nimpl\ninit\n") + self.assertEqual(self.out("status", "--porcelain"), "") + self.assertEqual((self.root / "tasks" / "archive.md").read_text().count("**t-three**"), 1) + + def test_main_tree_done_of_worktree_task_refused(self): + self.wf("status", "t-three", "progress", "sonnet/t-three", project=False, cwd=self.wt, env=IDENT) + for cmd in (["done", "t-three", "-m", "ok"], ["finish", "t-three", "-m", "ok", "--no-push"]): + code, out, err = self.wf(*cmd, project=False, cwd=self.root, env=IDENT) + self.assertEqual(code, 1, out + err) + self.assertIn("in progress in worktree", err) + self.assertIn("branch sonnet/t-three", err) + self.assertIn("t-three", (self.root / "TASKS.md").read_text()) + # from the worktree it goes through, books committed on master by the merge + self.write_code() + code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt", "--no-push") + self.assertEqual((code, err), (0, ""), out) + self.assertEqual(self.out("log", "--format=%s", "master"), "t-three done\nimpl\ninit\n") + + def test_main_tree_done_after_status_clear(self): + self.wf("status", "t-three", "progress", "sonnet/t-three", project=False, cwd=self.wt, env=IDENT) + self.wf("status", "t-three", "clear", project=False, cwd=self.root, env=IDENT) + code, out, err = self.wf("done", "t-three", "-m", "ok", project=False, cwd=self.root, env=IDENT) + self.assertEqual(code, 0, out + err) + + def test_main_tree_done_branch_without_worktree_ok(self): + self.wf("status", "t-three", "progress", "fast/t-three", project=False, cwd=self.root, env=IDENT) + code, out, err = self.wf("done", "t-three", "-m", "ok", project=False, cwd=self.root, env=IDENT) + self.assertEqual(code, 0, out + err) + + +if __name__ == "__main__": + unittest.main() |
