workflow

git clone https://git.godosa.eu/workflow

master

raw · 12071 bytes

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_new_untracked_dir_file_path_accepted(self):
        (self.wt / "docs" / "notes").mkdir(parents=True)
        (self.wt / "docs" / "notes" / "SCOPE.md").write_text("s\n")
        code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "docs/notes/SCOPE.md", "--no-push")
        self.assertEqual((code, err), (0, ""), out)
        self.assertIn("docs/notes/SCOPE.md", self.out("show", "--stat", "--format=", "master~1"))

    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()