1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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()
|