aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/tests/test_finish.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_finish.py
downloadworkflow-81d4e80fd5aabe4e80f58e960affa795cf7d34ec.tar.gz
workflow-81d4e80fd5aabe4e80f58e960affa795cf7d34ec.zip
workflow: initial public history
Diffstat (limited to 'tests/test_finish.py')
-rw-r--r--tests/test_finish.py210
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()