tasks-toolkit 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scripts/__init__.py +0 -0
- scripts/agents_sync.py +61 -0
- scripts/apply-plan.sh +51 -0
- scripts/build.sh +7 -0
- scripts/codex-task.sh +198 -0
- scripts/install-hooks.sh +9 -0
- scripts/junie-status.sh +52 -0
- scripts/junie-task.sh +183 -0
- scripts/kilo-task.sh +226 -0
- scripts/needs-dotnet-checks.sh +24 -0
- scripts/plan-sprint.sh +92 -0
- scripts/run-task.sh +79 -0
- scripts/run.sh +7 -0
- scripts/run_stats.py +141 -0
- scripts/run_summary.py +94 -0
- scripts/spec-lint.sh +139 -0
- scripts/sprint-auto.sh +79 -0
- scripts/sprint_auto_execute.py +160 -0
- scripts/sprint_auto_preflight.py +309 -0
- scripts/sprint_auto_prepare.py +271 -0
- scripts/sprint_auto_resume.py +153 -0
- scripts/sprint_auto_state.py +191 -0
- scripts/sprint_plan.py +238 -0
- scripts/task-report.sh +45 -0
- scripts/tasks.py +1813 -0
- scripts/tasks_cli.py +14 -0
- scripts/tasks_commands.py +11 -0
- scripts/tasks_init.py +140 -0
- scripts/tasks_mcp.py +309 -0
- scripts/tasks_model.py +173 -0
- scripts/tasks_plan_import.py +269 -0
- scripts/tasks_render.py +148 -0
- scripts/tasks_storage.py +373 -0
- scripts/tasks_watch.py +159 -0
- scripts/test.sh +43 -0
- scripts/test_agent_config.py +97 -0
- scripts/test_plan_sprint.sh +172 -0
- scripts/test_python_fast.sh +26 -0
- scripts/test_python_lint.sh +22 -0
- scripts/test_python_slow.sh +14 -0
- scripts/test_run_stats.py +64 -0
- scripts/test_runners.sh +588 -0
- scripts/test_spec_lint.sh +438 -0
- scripts/test_sprint_auto.py +50 -0
- scripts/test_sprint_auto_execute.py +112 -0
- scripts/test_sprint_auto_preflight.py +143 -0
- scripts/test_sprint_auto_prepare.py +150 -0
- scripts/test_sprint_auto_resume.py +104 -0
- scripts/test_sprint_auto_state.py +157 -0
- scripts/test_tasks.py +1678 -0
- scripts/test_tasks_mcp.py +233 -0
- scripts/test_tasks_plan_import.py +129 -0
- scripts/test_tasks_storage.py +67 -0
- scripts/test_tasks_watch.py +239 -0
- scripts/test_worktree.sh +2112 -0
- scripts/worktree.sh +992 -0
- scripts/worktree_mr.py +133 -0
- scripts/worktree_path.py +70 -0
- scripts/worktree_sprint.py +70 -0
- tasks_toolkit-0.1.3.dist-info/METADATA +121 -0
- tasks_toolkit-0.1.3.dist-info/RECORD +65 -0
- tasks_toolkit-0.1.3.dist-info/WHEEL +5 -0
- tasks_toolkit-0.1.3.dist-info/entry_points.txt +4 -0
- tasks_toolkit-0.1.3.dist-info/licenses/LICENSE +21 -0
- tasks_toolkit-0.1.3.dist-info/top_level.txt +1 -0
scripts/test_worktree.sh
ADDED
|
@@ -0,0 +1,2112 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for scripts/worktree.sh — plain bash, no external dependencies.
|
|
3
|
+
# Each test runs in its own temporary git repo. Prints PASS/FAIL per case.
|
|
4
|
+
# Exit code: 0 if all pass, 1 if any fail.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
passed=0
|
|
8
|
+
failed=0
|
|
9
|
+
total=0
|
|
10
|
+
|
|
11
|
+
pass() { passed=$((passed + 1)); total=$((total + 1)); echo " PASS: $1"; }
|
|
12
|
+
fail() { failed=$((failed + 1)); total=$((total + 1)); echo " FAIL: $1 — $2"; }
|
|
13
|
+
|
|
14
|
+
# ── helpers ──────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
make_repo() {
|
|
17
|
+
# Create a temp git repo, copy worktree.sh into scripts/, and cd into it.
|
|
18
|
+
# Returns the repo path via the global variable $TEST_REPO.
|
|
19
|
+
TEST_REPO="$(mktemp -d)"
|
|
20
|
+
cd "$TEST_REPO"
|
|
21
|
+
git init -q -b main
|
|
22
|
+
git config user.name "test"
|
|
23
|
+
git config user.email "test@localhost"
|
|
24
|
+
# worktree.sh computes root as dirname(BASH_SOURCE)/.., so it must live in scripts/
|
|
25
|
+
mkdir -p scripts
|
|
26
|
+
cp "$PROJECT_ROOT/scripts/worktree.sh" scripts/
|
|
27
|
+
cp "$PROJECT_ROOT/scripts/needs-dotnet-checks.sh" scripts/
|
|
28
|
+
chmod +x scripts/worktree.sh scripts/needs-dotnet-checks.sh
|
|
29
|
+
mkdir -p tmp/worktrees tmp/meta tmp/patches
|
|
30
|
+
# like the real repo: the agent workspace tmp/ is git-ignored (keeps the tree "clean")
|
|
31
|
+
printf 'tmp/\n' > .gitignore
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
cleanup_repo() {
|
|
35
|
+
rm -rf "$TEST_REPO"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Simulate worktree.sh "new" without actually creating a git worktree
|
|
39
|
+
# (we just need the baseline commit hash and the meta file).
|
|
40
|
+
fake_new() {
|
|
41
|
+
local name="$1"
|
|
42
|
+
mkdir -p tmp/worktrees/"$name" tmp/meta
|
|
43
|
+
# The baseline is HEAD — the current state of the repo
|
|
44
|
+
git rev-parse HEAD > "tmp/meta/$name.base"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Simulate worktree.sh "patch" — generate a diff between baseline and current state.
|
|
48
|
+
# Only stage tracked files and untracked files outside tmp/ and scripts/.
|
|
49
|
+
fake_patch() {
|
|
50
|
+
local name="$1"
|
|
51
|
+
local base
|
|
52
|
+
base="$(cat "tmp/meta/$name.base")"
|
|
53
|
+
# Stage only project files (exclude tmp/ and scripts/ which are test infrastructure)
|
|
54
|
+
git add -A
|
|
55
|
+
git reset -q "tmp/" "scripts/" 2>/dev/null || true
|
|
56
|
+
git diff --cached --binary "$base" > "tmp/patches/$name.patch"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ── test cases ───────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
test_clean_apply_modified_new_deleted() {
|
|
62
|
+
# Apply a patch that modifies one file, adds a new one, and deletes another.
|
|
63
|
+
make_repo
|
|
64
|
+
trap cleanup_repo RETURN
|
|
65
|
+
|
|
66
|
+
# Seed the repo with three files
|
|
67
|
+
echo "line1" > file-a.txt
|
|
68
|
+
echo "line2" > file-b.txt
|
|
69
|
+
echo "line3" > file-c.txt
|
|
70
|
+
git add -A && git commit -q -m "initial"
|
|
71
|
+
|
|
72
|
+
# Create a "worktree" baseline at HEAD
|
|
73
|
+
fake_new "clean"
|
|
74
|
+
|
|
75
|
+
# Simulate agent changes in the "worktree"
|
|
76
|
+
echo "modified line1" > file-a.txt # modify
|
|
77
|
+
echo "brand new file" > file-d.txt # new
|
|
78
|
+
rm file-c.txt # delete
|
|
79
|
+
|
|
80
|
+
# Generate the patch
|
|
81
|
+
fake_patch "clean"
|
|
82
|
+
|
|
83
|
+
# Reset working tree to baseline (simulates main checkout at baseline state)
|
|
84
|
+
git reset --hard -q HEAD
|
|
85
|
+
|
|
86
|
+
# Apply the patch
|
|
87
|
+
if bash scripts/worktree.sh apply clean > /dev/null 2>&1; then
|
|
88
|
+
# Verify results
|
|
89
|
+
local a d c
|
|
90
|
+
a="$(cat file-a.txt)"
|
|
91
|
+
d="$(cat file-d.txt 2>/dev/null || true)"
|
|
92
|
+
c="$(cat file-c.txt 2>/dev/null || true)"
|
|
93
|
+
if [ "$a" = "modified line1" ] && [ "$d" = "brand new file" ] && [ -z "$c" ]; then
|
|
94
|
+
pass "clean apply of modified+new+deleted"
|
|
95
|
+
else
|
|
96
|
+
fail "clean apply of modified+new+deleted" "content mismatch: a='$a' d='$d' c='$c'"
|
|
97
|
+
fi
|
|
98
|
+
else
|
|
99
|
+
fail "clean apply of modified+new+deleted" "apply command failed"
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test_apply_with_unrelated_uncommitted_edits() {
|
|
104
|
+
# The original bug: main checkout has uncommitted edits to a file the patch touches,
|
|
105
|
+
# but those edits match the baseline state the patch was generated from.
|
|
106
|
+
# The patch should still apply cleanly.
|
|
107
|
+
make_repo
|
|
108
|
+
trap cleanup_repo RETURN
|
|
109
|
+
|
|
110
|
+
# Seed the repo
|
|
111
|
+
echo "original content" > shared.txt
|
|
112
|
+
echo "other file" > other.txt
|
|
113
|
+
git add -A && git commit -q -m "initial"
|
|
114
|
+
|
|
115
|
+
# Simulate: user edits shared.txt (uncommitted), then creates worktree.
|
|
116
|
+
# In real worktree.sh "new", uncommitted changes are carried to the worktree
|
|
117
|
+
# and committed as the baseline. We simulate that here.
|
|
118
|
+
echo "edited before worktree" > shared.txt
|
|
119
|
+
git add -A
|
|
120
|
+
local baseline_commit
|
|
121
|
+
baseline_commit="$(git -c user.name=test -c user.email=test commit -q -m "baseline")"
|
|
122
|
+
baseline_commit="$(git rev-parse HEAD)"
|
|
123
|
+
mkdir -p tmp/worktrees/unrelated tmp/meta
|
|
124
|
+
echo "$baseline_commit" > tmp/meta/unrelated.base
|
|
125
|
+
|
|
126
|
+
# Now simulate the agent modifying shared.txt further in the worktree
|
|
127
|
+
echo "agent edited shared" > shared.txt
|
|
128
|
+
git add -A
|
|
129
|
+
git reset -q "tmp/" "scripts/" 2>/dev/null || true
|
|
130
|
+
git diff --cached --binary "$baseline_commit" > tmp/patches/unrelated.patch
|
|
131
|
+
|
|
132
|
+
# Reset main checkout to the baseline state (matching what the worktree started from)
|
|
133
|
+
git reset --hard -q "$baseline_commit"
|
|
134
|
+
# Re-create the meta file (git reset --hard may have affected it)
|
|
135
|
+
echo "$baseline_commit" > tmp/meta/unrelated.base
|
|
136
|
+
|
|
137
|
+
# Apply — should succeed because main's content matches baseline
|
|
138
|
+
if bash scripts/worktree.sh apply unrelated > /dev/null 2>&1; then
|
|
139
|
+
local content
|
|
140
|
+
content="$(cat shared.txt)"
|
|
141
|
+
if [ "$content" = "agent edited shared" ]; then
|
|
142
|
+
pass "apply with unrelated uncommitted edits (original bug)"
|
|
143
|
+
else
|
|
144
|
+
fail "apply with unrelated uncommitted edits" "content='$content', expected 'agent edited shared'"
|
|
145
|
+
fi
|
|
146
|
+
else
|
|
147
|
+
fail "apply with unrelated uncommitted edits" "apply command failed"
|
|
148
|
+
fi
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
test_drift_after_creation() {
|
|
152
|
+
# If the main checkout changes a file after the worktree was created,
|
|
153
|
+
# apply should detect drift and abort with exit 2.
|
|
154
|
+
make_repo
|
|
155
|
+
trap cleanup_repo RETURN
|
|
156
|
+
|
|
157
|
+
echo "baseline content" > drift-file.txt
|
|
158
|
+
git add -A && git commit -q -m "initial"
|
|
159
|
+
|
|
160
|
+
fake_new "drift"
|
|
161
|
+
|
|
162
|
+
# Simulate agent changes
|
|
163
|
+
echo "agent changed" > drift-file.txt
|
|
164
|
+
fake_patch "drift"
|
|
165
|
+
|
|
166
|
+
# Reset to baseline
|
|
167
|
+
git reset --hard -q HEAD
|
|
168
|
+
|
|
169
|
+
# Now someone edits the file in main AFTER the worktree was created
|
|
170
|
+
echo "someone else edited" > drift-file.txt
|
|
171
|
+
|
|
172
|
+
# Apply should fail with exit 2
|
|
173
|
+
local rc=0
|
|
174
|
+
bash scripts/worktree.sh apply drift > /dev/null 2>&1 || rc=$?
|
|
175
|
+
if [ "$rc" -eq 2 ]; then
|
|
176
|
+
# Verify tree is untouched (still has "someone else edited")
|
|
177
|
+
local content
|
|
178
|
+
content="$(cat drift-file.txt)"
|
|
179
|
+
if [ "$content" = "someone else edited" ]; then
|
|
180
|
+
pass "drift after creation -> exit 2, tree untouched"
|
|
181
|
+
else
|
|
182
|
+
fail "drift after creation" "tree was modified despite drift"
|
|
183
|
+
fi
|
|
184
|
+
else
|
|
185
|
+
fail "drift after creation" "expected exit 2, got $rc"
|
|
186
|
+
fi
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
test_patch_does_not_apply() {
|
|
190
|
+
# A patch that conflicts with current content should yield exit 3.
|
|
191
|
+
# The file must match the baseline (no drift), but the patch itself must not apply.
|
|
192
|
+
make_repo
|
|
193
|
+
trap cleanup_repo RETURN
|
|
194
|
+
|
|
195
|
+
echo "line A" > conflict.txt
|
|
196
|
+
echo "line B" >> conflict.txt
|
|
197
|
+
echo "line C" >> conflict.txt
|
|
198
|
+
echo "line D" >> conflict.txt
|
|
199
|
+
git add -A && git commit -q -m "initial"
|
|
200
|
+
|
|
201
|
+
fake_new "bad-patch"
|
|
202
|
+
|
|
203
|
+
# Create a patch against a different version of the file that won't apply
|
|
204
|
+
# to the current content. We craft a patch whose context lines don't exist.
|
|
205
|
+
cat > tmp/patches/bad-patch.patch <<'PATCH'
|
|
206
|
+
diff --git a/conflict.txt b/conflict.txt
|
|
207
|
+
index 0000000..1111111 100644
|
|
208
|
+
--- a/conflict.txt
|
|
209
|
+
+++ b/conflict.txt
|
|
210
|
+
@@ -1,4 +1,4 @@
|
|
211
|
+
XXXX NOT IN FILE
|
|
212
|
+
-line B
|
|
213
|
+
+REPLACED B
|
|
214
|
+
YYYY NOT IN FILE
|
|
215
|
+
-line D
|
|
216
|
+
+REPLACED D
|
|
217
|
+
PATCH
|
|
218
|
+
|
|
219
|
+
# File matches baseline (no drift), but patch context doesn't match
|
|
220
|
+
local rc=0
|
|
221
|
+
bash scripts/worktree.sh apply bad-patch > /dev/null 2>&1 || rc=$?
|
|
222
|
+
if [ "$rc" -eq 3 ]; then
|
|
223
|
+
# Verify tree is untouched
|
|
224
|
+
local content
|
|
225
|
+
content="$(cat conflict.txt)"
|
|
226
|
+
if [ "$content" = "line A
|
|
227
|
+
line B
|
|
228
|
+
line C
|
|
229
|
+
line D" ]; then
|
|
230
|
+
pass "patch that does not apply -> exit 3, tree untouched"
|
|
231
|
+
else
|
|
232
|
+
fail "patch that does not apply" "tree was modified"
|
|
233
|
+
fi
|
|
234
|
+
else
|
|
235
|
+
fail "patch that does not apply" "expected exit 3, got $rc"
|
|
236
|
+
fi
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
test_missing_patch() {
|
|
240
|
+
# No patch file -> exit 1.
|
|
241
|
+
make_repo
|
|
242
|
+
trap cleanup_repo RETURN
|
|
243
|
+
|
|
244
|
+
echo "x" > f.txt
|
|
245
|
+
git add -A && git commit -q -m "initial"
|
|
246
|
+
fake_new "no-patch"
|
|
247
|
+
# Don't create the patch file
|
|
248
|
+
|
|
249
|
+
local rc=0
|
|
250
|
+
bash scripts/worktree.sh apply no-patch > /dev/null 2>&1 || rc=$?
|
|
251
|
+
if [ "$rc" -eq 1 ]; then
|
|
252
|
+
pass "missing patch -> exit 1"
|
|
253
|
+
else
|
|
254
|
+
fail "missing patch" "expected exit 1, got $rc"
|
|
255
|
+
fi
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
test_dry_run_changes_nothing() {
|
|
259
|
+
# --dry-run should check the patch but not modify any files.
|
|
260
|
+
make_repo
|
|
261
|
+
trap cleanup_repo RETURN
|
|
262
|
+
|
|
263
|
+
echo "before" > dryfile.txt
|
|
264
|
+
git add -A && git commit -q -m "initial"
|
|
265
|
+
|
|
266
|
+
fake_new "dry"
|
|
267
|
+
echo "after" > dryfile.txt
|
|
268
|
+
fake_patch "dry"
|
|
269
|
+
|
|
270
|
+
# Reset to baseline
|
|
271
|
+
git reset --hard -q HEAD
|
|
272
|
+
|
|
273
|
+
local rc=0
|
|
274
|
+
bash scripts/worktree.sh apply --dry-run dry > /dev/null 2>&1 || rc=$?
|
|
275
|
+
if [ "$rc" -eq 0 ]; then
|
|
276
|
+
local content
|
|
277
|
+
content="$(cat dryfile.txt)"
|
|
278
|
+
if [ "$content" = "before" ]; then
|
|
279
|
+
pass "--dry-run changes nothing"
|
|
280
|
+
else
|
|
281
|
+
fail "--dry-run changes nothing" "file was modified: '$content'"
|
|
282
|
+
fi
|
|
283
|
+
else
|
|
284
|
+
fail "--dry-run changes nothing" "expected exit 0, got $rc"
|
|
285
|
+
fi
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
test_index_unchanged_after_apply() {
|
|
289
|
+
# After apply, git diff --cached should be empty (nothing staged).
|
|
290
|
+
make_repo
|
|
291
|
+
trap cleanup_repo RETURN
|
|
292
|
+
|
|
293
|
+
echo "original" > index-test.txt
|
|
294
|
+
git add -A && git commit -q -m "initial"
|
|
295
|
+
|
|
296
|
+
fake_new "idx"
|
|
297
|
+
echo "modified" > index-test.txt
|
|
298
|
+
fake_patch "idx"
|
|
299
|
+
|
|
300
|
+
# Reset to baseline
|
|
301
|
+
git reset --hard -q HEAD
|
|
302
|
+
|
|
303
|
+
bash scripts/worktree.sh apply idx > /dev/null 2>&1
|
|
304
|
+
|
|
305
|
+
# Check index is clean
|
|
306
|
+
local cached
|
|
307
|
+
cached="$(git diff --cached --stat)"
|
|
308
|
+
if [ -z "$cached" ]; then
|
|
309
|
+
# But working tree should have the change
|
|
310
|
+
local content
|
|
311
|
+
content="$(cat index-test.txt)"
|
|
312
|
+
if [ "$content" = "modified" ]; then
|
|
313
|
+
pass "index stays unchanged after apply"
|
|
314
|
+
else
|
|
315
|
+
fail "index stays unchanged" "working tree not modified"
|
|
316
|
+
fi
|
|
317
|
+
else
|
|
318
|
+
fail "index stays unchanged" "index has changes: $cached"
|
|
319
|
+
fi
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
test_missing_baseline_meta() {
|
|
323
|
+
# Missing tmp/meta/<name>.base -> exit 1.
|
|
324
|
+
make_repo
|
|
325
|
+
trap cleanup_repo RETURN
|
|
326
|
+
|
|
327
|
+
echo "x" > f.txt
|
|
328
|
+
git add -A && git commit -q -m "initial"
|
|
329
|
+
# Create a patch but no meta file
|
|
330
|
+
mkdir -p tmp/patches
|
|
331
|
+
echo "some patch content" > tmp/patches/no-meta.patch
|
|
332
|
+
|
|
333
|
+
local rc=0
|
|
334
|
+
bash scripts/worktree.sh apply no-meta > /dev/null 2>&1 || rc=$?
|
|
335
|
+
if [ "$rc" -eq 1 ]; then
|
|
336
|
+
pass "missing baseline meta -> exit 1"
|
|
337
|
+
else
|
|
338
|
+
fail "missing baseline meta" "expected exit 1, got $rc"
|
|
339
|
+
fi
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# ── path validation helpers ──────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
# Create a patch file that adds a single file with the given path.
|
|
345
|
+
# Args: $1=patch name, $2=file path inside the patch
|
|
346
|
+
make_add_patch() {
|
|
347
|
+
local name="$1"
|
|
348
|
+
local fpath="$2"
|
|
349
|
+
mkdir -p "tmp/patches"
|
|
350
|
+
cat > "tmp/patches/$name.patch" <<EOF
|
|
351
|
+
diff --git a/$fpath b/$fpath
|
|
352
|
+
new file mode 100644
|
|
353
|
+
index 0000000..e69de29
|
|
354
|
+
--- /dev/null
|
|
355
|
+
+++ b/$fpath
|
|
356
|
+
@@ -0,0 +1 @@
|
|
357
|
+
+hello
|
|
358
|
+
EOF
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Create a patch file that deletes a file.
|
|
362
|
+
# Args: $1=patch name, $2=file path
|
|
363
|
+
make_delete_patch() {
|
|
364
|
+
local name="$1"
|
|
365
|
+
local fpath="$2"
|
|
366
|
+
mkdir -p "tmp/patches"
|
|
367
|
+
cat > "tmp/patches/$name.patch" <<EOF
|
|
368
|
+
diff --git a/$fpath b/$fpath
|
|
369
|
+
deleted file mode 100644
|
|
370
|
+
index e69de29..0000000
|
|
371
|
+
--- a/$fpath
|
|
372
|
+
+++ /dev/null
|
|
373
|
+
@@ -1 +0,0 @@
|
|
374
|
+
-hello
|
|
375
|
+
EOF
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Create a patch file that renames a file.
|
|
379
|
+
# Args: $1=patch name, $2=source path, $3=dest path
|
|
380
|
+
make_rename_patch() {
|
|
381
|
+
local name="$1"
|
|
382
|
+
local src="$2"
|
|
383
|
+
local dst="$3"
|
|
384
|
+
mkdir -p "tmp/patches"
|
|
385
|
+
cat > "tmp/patches/$name.patch" <<EOF
|
|
386
|
+
diff --git a/$dst b/$dst
|
|
387
|
+
similarity index 100%
|
|
388
|
+
rename from $src
|
|
389
|
+
rename to $dst
|
|
390
|
+
EOF
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Create a patch file that adds a symlink.
|
|
394
|
+
# Args: $1=patch name, $2=file path, $3=link target
|
|
395
|
+
make_symlink_patch() {
|
|
396
|
+
local name="$1"
|
|
397
|
+
local fpath="$2"
|
|
398
|
+
local target="$3"
|
|
399
|
+
mkdir -p "tmp/patches"
|
|
400
|
+
cat > "tmp/patches/$name.patch" <<EOF
|
|
401
|
+
diff --git a/$fpath b/$fpath
|
|
402
|
+
new file mode 120000
|
|
403
|
+
index 0000000..e69de29
|
|
404
|
+
--- /dev/null
|
|
405
|
+
+++ b/$fpath
|
|
406
|
+
@@ -0,0 +1 @@
|
|
407
|
+
+${target}
|
|
408
|
+
EOF
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# ── path validation test cases ───────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
# Helper: source worktree.sh helpers and test path_ok directly.
|
|
414
|
+
# Args: $1=test name, $2=path to test, $3=expected result (0=ok, 1=forbidden)
|
|
415
|
+
assert_path_ok() {
|
|
416
|
+
local test_name="$1"
|
|
417
|
+
local test_path="$2"
|
|
418
|
+
local expected="$3"
|
|
419
|
+
# Source just the helper functions from worktree.sh
|
|
420
|
+
# We need GENERATED_PATHS and path_ok + is_generated_path
|
|
421
|
+
local saved_root="$root"
|
|
422
|
+
root="$TEST_REPO"
|
|
423
|
+
if path_ok "$test_path"; then
|
|
424
|
+
local actual=0
|
|
425
|
+
else
|
|
426
|
+
local actual=1
|
|
427
|
+
fi
|
|
428
|
+
root="$saved_root"
|
|
429
|
+
if [ "$actual" -eq "$expected" ]; then
|
|
430
|
+
pass "$test_name -> path_ok returns $expected"
|
|
431
|
+
else
|
|
432
|
+
fail "$test_name" "expected $expected, got $actual for path '$test_path'"
|
|
433
|
+
fi
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
test_forbidden_githooks_pre_commit() {
|
|
437
|
+
make_repo; trap cleanup_repo RETURN
|
|
438
|
+
assert_path_ok ".githooks/pre-commit" ".githooks/pre-commit" 1
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
test_forbidden_git_hooks() {
|
|
442
|
+
make_repo; trap cleanup_repo RETURN
|
|
443
|
+
assert_path_ok ".git/hooks/x" ".git/hooks/x" 1
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
test_forbidden_dotdot() {
|
|
447
|
+
make_repo; trap cleanup_repo RETURN
|
|
448
|
+
assert_path_ok "../outside.txt" "../outside.txt" 1
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
test_forbidden_absolute_path() {
|
|
452
|
+
make_repo; trap cleanup_repo RETURN
|
|
453
|
+
assert_path_ok "/abs/path" "/abs/path" 1
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
test_forbidden_space_in_name() {
|
|
457
|
+
make_repo; trap cleanup_repo RETURN
|
|
458
|
+
assert_path_ok "a b.txt" "a b.txt" 1
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
test_forbidden_non_ascii() {
|
|
462
|
+
make_repo; trap cleanup_repo RETURN
|
|
463
|
+
assert_path_ok "café.txt" "café.txt" 1
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
test_forbidden_dash_prefix() {
|
|
467
|
+
make_repo; trap cleanup_repo RETURN
|
|
468
|
+
assert_path_ok "-bad/file.txt" "-bad/file.txt" 1
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
test_forbidden_tmp() {
|
|
472
|
+
make_repo; trap cleanup_repo RETURN
|
|
473
|
+
assert_path_ok "tmp/x" "tmp/x" 1
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
test_forbidden_claude_settings() {
|
|
477
|
+
make_repo; trap cleanup_repo RETURN
|
|
478
|
+
assert_path_ok ".claude/settings.json" ".claude/settings.json" 1
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
test_forbidden_kilo_json() {
|
|
482
|
+
make_repo; trap cleanup_repo RETURN
|
|
483
|
+
assert_path_ok "kilo.json" "kilo.json" 1
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
test_forbidden_symlink_outside_scripts() {
|
|
487
|
+
# Symlinks outside scripts/ are rejected by validate_symlinks, not path_ok.
|
|
488
|
+
# path_ok itself allows src/link — the symlink check is a separate layer.
|
|
489
|
+
# Test that path_ok allows the path (symlink validation is in apply/mr).
|
|
490
|
+
make_repo; trap cleanup_repo RETURN
|
|
491
|
+
assert_path_ok "src/link (symlink path ok)" "src/link" 0
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
test_forbidden_double_slash() {
|
|
495
|
+
make_repo; trap cleanup_repo RETURN
|
|
496
|
+
assert_path_ok "a//b.txt (double slash)" "a//b.txt" 1
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
test_forbidden_junie_dir() {
|
|
500
|
+
make_repo; trap cleanup_repo RETURN
|
|
501
|
+
assert_path_ok ".junie/config.json" ".junie/config.json" 1
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
test_forbidden_mcp_json() {
|
|
505
|
+
make_repo; trap cleanup_repo RETURN
|
|
506
|
+
assert_path_ok ".mcp.json" ".mcp.json" 1
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
test_forbidden_tasks_db() {
|
|
510
|
+
make_repo; trap cleanup_repo RETURN
|
|
511
|
+
assert_path_ok "tasks/tasks.db" "tasks/tasks.db" 1
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
test_forbidden_tasks_lock() {
|
|
515
|
+
make_repo; trap cleanup_repo RETURN
|
|
516
|
+
assert_path_ok "tasks/.lock" "tasks/.lock" 1
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
test_forbidden_kilo_dir() {
|
|
520
|
+
make_repo; trap cleanup_repo RETURN
|
|
521
|
+
assert_path_ok ".kilo/settings.json" ".kilo/settings.json" 1
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# Allowed paths — each must return 0 from path_ok
|
|
525
|
+
test_allowed_src_path() {
|
|
526
|
+
make_repo; trap cleanup_repo RETURN
|
|
527
|
+
assert_path_ok "src/A/B.cs" "src/A/B.cs" 0
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
test_allowed_scripts_path() {
|
|
531
|
+
make_repo; trap cleanup_repo RETURN
|
|
532
|
+
assert_path_ok "scripts/x.sh" "scripts/x.sh" 0
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
test_allowed_knowledge_path() {
|
|
536
|
+
make_repo; trap cleanup_repo RETURN
|
|
537
|
+
assert_path_ok "knowledge/n.md" "knowledge/n.md" 0
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
test_allowed_tests_path() {
|
|
541
|
+
make_repo; trap cleanup_repo RETURN
|
|
542
|
+
assert_path_ok "tests/T/File.Name.cs" "tests/T/File.Name.cs" 0
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
test_allowed_editorconfig() {
|
|
546
|
+
make_repo; trap cleanup_repo RETURN
|
|
547
|
+
assert_path_ok ".editorconfig" ".editorconfig" 0
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
test_rename_allowed_path() {
|
|
551
|
+
# Both source and dest of a rename must pass path_ok
|
|
552
|
+
make_repo; trap cleanup_repo RETURN
|
|
553
|
+
assert_path_ok "src/old.cs (rename src)" "src/old.cs" 0
|
|
554
|
+
assert_path_ok "src/new.cs (rename dst)" "src/new.cs" 0
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
test_delete_allowed_path() {
|
|
558
|
+
make_repo; trap cleanup_repo RETURN
|
|
559
|
+
assert_path_ok "src/deleteme.cs (delete)" "src/deleteme.cs" 0
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
test_dry_run_forbidden_path() {
|
|
563
|
+
# Old test for apply --dry-run with forbidden path — apply is retired.
|
|
564
|
+
# Keep as a no-op placeholder.
|
|
565
|
+
: # intentionally empty
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
# ── sprint lifecycle tests ───────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
test_sprint_start_creates_branch() {
|
|
571
|
+
make_repo
|
|
572
|
+
trap cleanup_repo RETURN
|
|
573
|
+
|
|
574
|
+
echo "initial" > file.txt
|
|
575
|
+
git add -A && git commit -q -m "initial"
|
|
576
|
+
|
|
577
|
+
local out
|
|
578
|
+
out="$(bash scripts/worktree.sh sprint start 1 test)"
|
|
579
|
+
if [[ "$out" == "sprint sprint/1-test (from main)" ]]; then
|
|
580
|
+
local branch
|
|
581
|
+
branch="$(git symbolic-ref --short HEAD)"
|
|
582
|
+
if [ "$branch" = "sprint/1-test" ]; then
|
|
583
|
+
pass "sprint start creates and checks out sprint branch"
|
|
584
|
+
else
|
|
585
|
+
fail "sprint start checkout" "on branch '$branch', expected 'sprint/1-test'"
|
|
586
|
+
fi
|
|
587
|
+
else
|
|
588
|
+
fail "sprint start output" "got '$out'"
|
|
589
|
+
fi
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
test_sprint_start_refuses_existing() {
|
|
593
|
+
make_repo
|
|
594
|
+
trap cleanup_repo RETURN
|
|
595
|
+
|
|
596
|
+
echo "initial" > file.txt
|
|
597
|
+
git add -A && git commit -q -m "initial"
|
|
598
|
+
|
|
599
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
600
|
+
local rc=0
|
|
601
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null 2>&1 || rc=$?
|
|
602
|
+
if [ "$rc" -eq 1 ]; then
|
|
603
|
+
pass "sprint start refuses existing branch"
|
|
604
|
+
else
|
|
605
|
+
fail "sprint start refuses existing" "expected exit 1, got $rc"
|
|
606
|
+
fi
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
test_sprint_start_refuses_dirty_tree() {
|
|
610
|
+
make_repo
|
|
611
|
+
trap cleanup_repo RETURN
|
|
612
|
+
|
|
613
|
+
echo "initial" > file.txt
|
|
614
|
+
git add -A && git commit -q -m "initial"
|
|
615
|
+
echo "dirty" > dirty.txt
|
|
616
|
+
|
|
617
|
+
local rc=0
|
|
618
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null 2>&1 || rc=$?
|
|
619
|
+
if [ "$rc" -eq 1 ]; then
|
|
620
|
+
pass "sprint start refuses dirty tree"
|
|
621
|
+
else
|
|
622
|
+
fail "sprint start refuses dirty" "expected exit 1, got $rc"
|
|
623
|
+
fi
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
test_sprint_start_ignores_generated_paths() {
|
|
627
|
+
make_repo
|
|
628
|
+
trap cleanup_repo RETURN
|
|
629
|
+
|
|
630
|
+
echo "initial" > file.txt
|
|
631
|
+
git add -A && git commit -q -m "initial"
|
|
632
|
+
# Modify a generated path — should be ignored by clean check
|
|
633
|
+
mkdir -p tasks
|
|
634
|
+
echo "modified" > tasks/tasks.snapshot.json
|
|
635
|
+
|
|
636
|
+
local rc=0
|
|
637
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null 2>&1 || rc=$?
|
|
638
|
+
if [ "$rc" -eq 0 ]; then
|
|
639
|
+
pass "sprint start ignores generated paths in clean check"
|
|
640
|
+
else
|
|
641
|
+
fail "sprint start ignores generated" "expected exit 0, got $rc"
|
|
642
|
+
fi
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
test_sprint_status_on_sprint() {
|
|
646
|
+
make_repo
|
|
647
|
+
trap cleanup_repo RETURN
|
|
648
|
+
|
|
649
|
+
echo "initial" > file.txt
|
|
650
|
+
git add -A && git commit -q -m "initial"
|
|
651
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
652
|
+
|
|
653
|
+
local out
|
|
654
|
+
out="$(bash scripts/worktree.sh sprint status)"
|
|
655
|
+
if printf '%s\n' "$out" | grep -q "branch: sprint/1-test"; then
|
|
656
|
+
pass "sprint status shows sprint branch"
|
|
657
|
+
else
|
|
658
|
+
fail "sprint status" "output: $out"
|
|
659
|
+
fi
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
test_sprint_status_off_sprint() {
|
|
663
|
+
make_repo
|
|
664
|
+
trap cleanup_repo RETURN
|
|
665
|
+
|
|
666
|
+
echo "initial" > file.txt
|
|
667
|
+
git add -A && git commit -q -m "initial"
|
|
668
|
+
|
|
669
|
+
local out
|
|
670
|
+
out="$(bash scripts/worktree.sh sprint status)"
|
|
671
|
+
if printf '%s\n' "$out" | grep -q "(not on a sprint branch)"; then
|
|
672
|
+
pass "sprint status off sprint"
|
|
673
|
+
else
|
|
674
|
+
fail "sprint status off sprint" "output: $out"
|
|
675
|
+
fi
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
test_sprint_finish_no_merge() {
|
|
679
|
+
make_repo
|
|
680
|
+
trap cleanup_repo RETURN
|
|
681
|
+
|
|
682
|
+
# Create main branch for the finish command
|
|
683
|
+
echo "initial" > file.txt
|
|
684
|
+
git add -A && git commit -q -m "initial"
|
|
685
|
+
git branch -m main 2>/dev/null || git branch -m master main 2>/dev/null || true
|
|
686
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
687
|
+
|
|
688
|
+
local out rc=0
|
|
689
|
+
out="$(bash scripts/worktree.sh sprint finish 2>&1)" || rc=$?
|
|
690
|
+
# Without dotnet, it should still print the no-merge message
|
|
691
|
+
if printf '%s\n' "$out" | grep -q "(no merge"; then
|
|
692
|
+
pass "sprint finish without --merge"
|
|
693
|
+
else
|
|
694
|
+
fail "sprint finish no merge" "rc=$rc output: $out"
|
|
695
|
+
fi
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
test_sprint_finish_merge() {
|
|
699
|
+
make_repo
|
|
700
|
+
trap cleanup_repo RETURN
|
|
701
|
+
|
|
702
|
+
# Set up main branch
|
|
703
|
+
echo "initial" > file.txt
|
|
704
|
+
git add -A && git commit -q -m "initial"
|
|
705
|
+
git branch -m main 2>/dev/null || true
|
|
706
|
+
|
|
707
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
708
|
+
# Make a commit on the sprint branch
|
|
709
|
+
echo "sprint work" > sprint-file.txt
|
|
710
|
+
git add -A && git commit -q -m "sprint work"
|
|
711
|
+
|
|
712
|
+
local out rc=0
|
|
713
|
+
out="$(bash scripts/worktree.sh sprint finish --merge 2>&1)" || rc=$?
|
|
714
|
+
if printf '%s\n' "$out" | grep -q "merged sprint/1-test into main"; then
|
|
715
|
+
local branch
|
|
716
|
+
branch="$(git symbolic-ref --short HEAD)"
|
|
717
|
+
if [ "$branch" = "main" ]; then
|
|
718
|
+
pass "sprint finish --merge merges and checks out main"
|
|
719
|
+
else
|
|
720
|
+
fail "sprint finish --merge checkout" "on branch '$branch'"
|
|
721
|
+
fi
|
|
722
|
+
else
|
|
723
|
+
fail "sprint finish --merge" "rc=$rc output: $out"
|
|
724
|
+
fi
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
test_sprint_finish_refuses_off_sprint() {
|
|
728
|
+
make_repo
|
|
729
|
+
trap cleanup_repo RETURN
|
|
730
|
+
|
|
731
|
+
echo "initial" > file.txt
|
|
732
|
+
git add -A && git commit -q -m "initial"
|
|
733
|
+
|
|
734
|
+
local rc=0
|
|
735
|
+
bash scripts/worktree.sh sprint finish >/dev/null 2>&1 || rc=$?
|
|
736
|
+
if [ "$rc" -eq 1 ]; then
|
|
737
|
+
pass "sprint finish refuses off sprint branch"
|
|
738
|
+
else
|
|
739
|
+
fail "sprint finish off sprint" "expected exit 1, got $rc"
|
|
740
|
+
fi
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
# ── new command tests (sprint-based) ─────────────────────────────────────
|
|
744
|
+
|
|
745
|
+
test_new_refuses_off_sprint() {
|
|
746
|
+
make_repo
|
|
747
|
+
trap cleanup_repo RETURN
|
|
748
|
+
|
|
749
|
+
echo "initial" > file.txt
|
|
750
|
+
git add -A && git commit -q -m "initial"
|
|
751
|
+
|
|
752
|
+
local rc=0
|
|
753
|
+
bash scripts/worktree.sh new my-task >/dev/null 2>&1 || rc=$?
|
|
754
|
+
if [ "$rc" -eq 1 ]; then
|
|
755
|
+
pass "new refuses off sprint branch"
|
|
756
|
+
else
|
|
757
|
+
fail "new refuses off sprint" "expected exit 1, got $rc"
|
|
758
|
+
fi
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
test_new_refuses_dirty_tree() {
|
|
762
|
+
make_repo
|
|
763
|
+
trap cleanup_repo RETURN
|
|
764
|
+
|
|
765
|
+
echo "initial" > file.txt
|
|
766
|
+
git add -A && git commit -q -m "initial"
|
|
767
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
768
|
+
echo "dirty" > dirty.txt
|
|
769
|
+
|
|
770
|
+
local rc=0
|
|
771
|
+
bash scripts/worktree.sh new my-task >/dev/null 2>&1 || rc=$?
|
|
772
|
+
if [ "$rc" -eq 1 ]; then
|
|
773
|
+
pass "new refuses dirty tree"
|
|
774
|
+
else
|
|
775
|
+
fail "new refuses dirty" "expected exit 1, got $rc"
|
|
776
|
+
fi
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
test_new_ignores_generated_paths() {
|
|
780
|
+
make_repo
|
|
781
|
+
trap cleanup_repo RETURN
|
|
782
|
+
|
|
783
|
+
echo "initial" > file.txt
|
|
784
|
+
git add -A && git commit -q -m "initial"
|
|
785
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
786
|
+
# Modify a generated path — should be ignored
|
|
787
|
+
mkdir -p tasks
|
|
788
|
+
echo "modified" > tasks/tasks.snapshot.json
|
|
789
|
+
|
|
790
|
+
local out rc=0
|
|
791
|
+
out="$(bash scripts/worktree.sh new my-task 2>&1)" || rc=$?
|
|
792
|
+
if [ "$rc" -eq 0 ]; then
|
|
793
|
+
pass "new ignores generated paths in clean check"
|
|
794
|
+
else
|
|
795
|
+
fail "new ignores generated" "expected exit 0, rc=$rc output: $out"
|
|
796
|
+
fi
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
test_new_creates_task_branch_and_meta() {
|
|
800
|
+
make_repo
|
|
801
|
+
trap cleanup_repo RETURN
|
|
802
|
+
|
|
803
|
+
echo "initial" > file.txt
|
|
804
|
+
git add -A && git commit -q -m "initial"
|
|
805
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
806
|
+
|
|
807
|
+
local out
|
|
808
|
+
out="$(bash scripts/worktree.sh new my-task)"
|
|
809
|
+
if [ -d "$TEST_REPO/tmp/worktrees/my-task" ]; then
|
|
810
|
+
# Check branch exists
|
|
811
|
+
if git rev-parse --verify "task/my-task" >/dev/null 2>&1; then
|
|
812
|
+
# Check meta files
|
|
813
|
+
local base sprint
|
|
814
|
+
base="$(cat tmp/meta/my-task.base)"
|
|
815
|
+
sprint="$(cat tmp/meta/my-task.sprint)"
|
|
816
|
+
local sprint_head
|
|
817
|
+
sprint_head="$(git rev-parse HEAD)"
|
|
818
|
+
if [ "$base" = "$sprint_head" ] && [ "$sprint" = "sprint/1-test" ]; then
|
|
819
|
+
pass "new creates task branch, worktree and meta files"
|
|
820
|
+
else
|
|
821
|
+
fail "new meta files" "base=$base sprint=$sprint sprint_head=$sprint_head"
|
|
822
|
+
fi
|
|
823
|
+
else
|
|
824
|
+
fail "new branch" "task/my-task branch not found"
|
|
825
|
+
fi
|
|
826
|
+
else
|
|
827
|
+
fail "new worktree" "worktree dir not found: $out"
|
|
828
|
+
fi
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
test_new_with_task_id() {
|
|
832
|
+
make_repo
|
|
833
|
+
trap cleanup_repo RETURN
|
|
834
|
+
|
|
835
|
+
echo "initial" > file.txt
|
|
836
|
+
git add -A && git commit -q -m "initial"
|
|
837
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
838
|
+
|
|
839
|
+
bash scripts/worktree.sh new my-task --task-id 1.3 >/dev/null
|
|
840
|
+
if [ -f "tmp/meta/my-task.task" ]; then
|
|
841
|
+
local tid
|
|
842
|
+
tid="$(cat tmp/meta/my-task.task)"
|
|
843
|
+
if [ "$tid" = "1.3" ]; then
|
|
844
|
+
pass "new stores --task-id in meta"
|
|
845
|
+
else
|
|
846
|
+
fail "new task-id" "got '$tid', expected '1.3'"
|
|
847
|
+
fi
|
|
848
|
+
else
|
|
849
|
+
fail "new task-id" "tmp/meta/my-task.task not found"
|
|
850
|
+
fi
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
test_new_baseline_is_sprint_head_not_carried() {
|
|
854
|
+
# Uncommitted changes on sprint branch should NOT be carried to the worktree.
|
|
855
|
+
# The baseline is the sprint HEAD commit, not the dirty working tree.
|
|
856
|
+
make_repo
|
|
857
|
+
trap cleanup_repo RETURN
|
|
858
|
+
|
|
859
|
+
echo "initial" > file.txt
|
|
860
|
+
git add -A && git commit -q -m "initial"
|
|
861
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
862
|
+
|
|
863
|
+
# Make uncommitted changes on the sprint branch — these should NOT be carried
|
|
864
|
+
echo "uncommitted junk" > junk.txt
|
|
865
|
+
echo "modified initial" > file.txt
|
|
866
|
+
|
|
867
|
+
# tree_clean will reject because of junk.txt and modified file.txt.
|
|
868
|
+
# But if we only dirty with generated files, new should succeed and the
|
|
869
|
+
# worktree should have the committed state (sprint HEAD), not the dirty state.
|
|
870
|
+
# First, let's test that non-generated dirty state IS rejected:
|
|
871
|
+
local rc=0
|
|
872
|
+
bash scripts/worktree.sh new reject-dirty >/dev/null 2>&1 || rc=$?
|
|
873
|
+
if [ "$rc" -eq 1 ]; then
|
|
874
|
+
# Good — dirty tree is rejected. Now clean up and test with only generated dirt.
|
|
875
|
+
rm -f junk.txt
|
|
876
|
+
echo "initial" > file.txt # restore to committed state
|
|
877
|
+
mkdir -p tasks
|
|
878
|
+
echo "generated" > tasks/tasks.snapshot.json
|
|
879
|
+
|
|
880
|
+
bash scripts/worktree.sh new my-task >/dev/null
|
|
881
|
+
|
|
882
|
+
# The worktree should have the committed state
|
|
883
|
+
local wt="$TEST_REPO/tmp/worktrees/my-task"
|
|
884
|
+
if [ -d "$wt" ]; then
|
|
885
|
+
local wt_content
|
|
886
|
+
wt_content="$(cat "$wt/file.txt")"
|
|
887
|
+
if [ "$wt_content" = "initial" ]; then
|
|
888
|
+
pass "new baseline = sprint HEAD, uncommitted changes NOT carried"
|
|
889
|
+
else
|
|
890
|
+
fail "new baseline content" "worktree has '$wt_content', expected 'initial'"
|
|
891
|
+
fi
|
|
892
|
+
else
|
|
893
|
+
fail "new worktree" "worktree dir not created"
|
|
894
|
+
fi
|
|
895
|
+
else
|
|
896
|
+
fail "new rejects dirty tree" "expected exit 1 for dirty tree, got $rc"
|
|
897
|
+
fi
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
test_new_refuses_existing_worktree() {
|
|
901
|
+
make_repo
|
|
902
|
+
trap cleanup_repo RETURN
|
|
903
|
+
|
|
904
|
+
echo "initial" > file.txt
|
|
905
|
+
git add -A && git commit -q -m "initial"
|
|
906
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
907
|
+
bash scripts/worktree.sh new my-task >/dev/null
|
|
908
|
+
|
|
909
|
+
local rc=0
|
|
910
|
+
bash scripts/worktree.sh new my-task >/dev/null 2>&1 || rc=$?
|
|
911
|
+
if [ "$rc" -eq 1 ]; then
|
|
912
|
+
pass "new refuses existing worktree"
|
|
913
|
+
else
|
|
914
|
+
fail "new refuses existing" "expected exit 1, got $rc"
|
|
915
|
+
fi
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
# ── GENERATED_PATHS tests ────────────────────────────────────────────────
|
|
919
|
+
|
|
920
|
+
test_generated_paths_forbidden_in_patch() {
|
|
921
|
+
# GENERATED_PATHS should be rejected by path_ok
|
|
922
|
+
make_repo
|
|
923
|
+
trap cleanup_repo RETURN
|
|
924
|
+
assert_path_ok "tasks/tasks.snapshot.json (generated)" "tasks/tasks.snapshot.json" 1
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
test_generated_paths_forbidden_view() {
|
|
928
|
+
make_repo
|
|
929
|
+
trap cleanup_repo RETURN
|
|
930
|
+
assert_path_ok "tasks/README.md (generated)" "tasks/README.md" 1
|
|
931
|
+
assert_path_ok "tasks/sprint-1-x.md (generated)" "tasks/sprint-1-x.md" 1
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
test_clean_tree_ignores_generated_snapshot() {
|
|
935
|
+
# tree_clean should ignore tasks/tasks.snapshot.json
|
|
936
|
+
make_repo
|
|
937
|
+
trap cleanup_repo RETURN
|
|
938
|
+
|
|
939
|
+
echo "initial" > file.txt
|
|
940
|
+
git add -A && git commit -q -m "initial"
|
|
941
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
942
|
+
|
|
943
|
+
# Modify generated file — tree should be considered clean
|
|
944
|
+
mkdir -p tasks
|
|
945
|
+
echo "modified snapshot" > tasks/tasks.snapshot.json
|
|
946
|
+
|
|
947
|
+
local rc=0
|
|
948
|
+
bash scripts/worktree.sh new clean-gen >/dev/null 2>&1 || rc=$?
|
|
949
|
+
if [ "$rc" -eq 0 ]; then
|
|
950
|
+
pass "clean tree ignores tasks/tasks.snapshot.json"
|
|
951
|
+
else
|
|
952
|
+
fail "clean tree ignores snapshot" "expected exit 0, got $rc"
|
|
953
|
+
fi
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
test_clean_tree_ignores_generated_view() {
|
|
957
|
+
make_repo
|
|
958
|
+
trap cleanup_repo RETURN
|
|
959
|
+
|
|
960
|
+
echo "initial" > file.txt
|
|
961
|
+
git add -A && git commit -q -m "initial"
|
|
962
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
963
|
+
|
|
964
|
+
mkdir -p tasks
|
|
965
|
+
echo "modified view" > tasks/README.md
|
|
966
|
+
|
|
967
|
+
local rc=0
|
|
968
|
+
bash scripts/worktree.sh new clean-view >/dev/null 2>&1 || rc=$?
|
|
969
|
+
if [ "$rc" -eq 0 ]; then
|
|
970
|
+
pass "clean tree ignores tasks/README.md"
|
|
971
|
+
else
|
|
972
|
+
fail "clean tree ignores view" "expected exit 0, got $rc"
|
|
973
|
+
fi
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
# ── abandon tests ────────────────────────────────────────────────────────
|
|
977
|
+
|
|
978
|
+
test_abandon_cleans_worktree_branch_meta() {
|
|
979
|
+
make_repo
|
|
980
|
+
trap cleanup_repo RETURN
|
|
981
|
+
|
|
982
|
+
echo "initial" > file.txt
|
|
983
|
+
git add -A && git commit -q -m "initial"
|
|
984
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
985
|
+
bash scripts/worktree.sh new my-task --task-id 1.3 >/dev/null
|
|
986
|
+
|
|
987
|
+
local out
|
|
988
|
+
out="$(bash scripts/worktree.sh abandon my-task)"
|
|
989
|
+
if [[ "$out" == "abandoned my-task" ]]; then
|
|
990
|
+
# Verify cleanup
|
|
991
|
+
local wt_exists=0 branch_exists=0 base_exists=0 sprint_exists=0 task_exists=0
|
|
992
|
+
[ -d "$TEST_REPO/tmp/worktrees/my-task" ] && wt_exists=1
|
|
993
|
+
git rev-parse --verify "task/my-task" >/dev/null 2>&1 && branch_exists=1
|
|
994
|
+
[ -f "tmp/meta/my-task.base" ] && base_exists=1
|
|
995
|
+
[ -f "tmp/meta/my-task.sprint" ] && sprint_exists=1
|
|
996
|
+
[ -f "tmp/meta/my-task.task" ] && task_exists=1
|
|
997
|
+
if [ "$wt_exists" -eq 0 ] && [ "$branch_exists" -eq 0 ] && \
|
|
998
|
+
[ "$base_exists" -eq 0 ] && [ "$sprint_exists" -eq 0 ] && [ "$task_exists" -eq 0 ]; then
|
|
999
|
+
pass "abandon cleans worktree, branch and all meta files"
|
|
1000
|
+
else
|
|
1001
|
+
fail "abandon cleanup" "wt=$wt_exists branch=$branch_exists base=$base_exists sprint=$sprint_exists task=$task_exists"
|
|
1002
|
+
fi
|
|
1003
|
+
else
|
|
1004
|
+
fail "abandon output" "got '$out'"
|
|
1005
|
+
fi
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
test_abandon_idempotent() {
|
|
1009
|
+
make_repo
|
|
1010
|
+
trap cleanup_repo RETURN
|
|
1011
|
+
|
|
1012
|
+
echo "initial" > file.txt
|
|
1013
|
+
git add -A && git commit -q -m "initial"
|
|
1014
|
+
|
|
1015
|
+
local rc=0
|
|
1016
|
+
bash scripts/worktree.sh abandon nonexistent >/dev/null 2>&1 || rc=$?
|
|
1017
|
+
if [ "$rc" -eq 0 ]; then
|
|
1018
|
+
pass "abandon is idempotent on nonexistent worktree"
|
|
1019
|
+
else
|
|
1020
|
+
fail "abandon idempotent" "expected exit 0, got $rc"
|
|
1021
|
+
fi
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
# ── remove still works ───────────────────────────────────────────────────
|
|
1025
|
+
|
|
1026
|
+
test_remove_still_works() {
|
|
1027
|
+
make_repo
|
|
1028
|
+
trap cleanup_repo RETURN
|
|
1029
|
+
|
|
1030
|
+
echo "initial" > file.txt
|
|
1031
|
+
git add -A && git commit -q -m "initial"
|
|
1032
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
1033
|
+
bash scripts/worktree.sh new my-task >/dev/null
|
|
1034
|
+
|
|
1035
|
+
local out
|
|
1036
|
+
out="$(bash scripts/worktree.sh remove my-task)"
|
|
1037
|
+
if [[ "$out" == "removed my-task" ]]; then
|
|
1038
|
+
local wt_exists=0 branch_exists=0
|
|
1039
|
+
[ -d "$TEST_REPO/tmp/worktrees/my-task" ] && wt_exists=1
|
|
1040
|
+
git rev-parse --verify "task/my-task" >/dev/null 2>&1 && branch_exists=1
|
|
1041
|
+
if [ "$wt_exists" -eq 0 ] && [ "$branch_exists" -eq 0 ]; then
|
|
1042
|
+
pass "remove still works with task branches"
|
|
1043
|
+
else
|
|
1044
|
+
fail "remove cleanup" "wt=$wt_exists branch=$branch_exists"
|
|
1045
|
+
fi
|
|
1046
|
+
else
|
|
1047
|
+
fail "remove output" "got '$out'"
|
|
1048
|
+
fi
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
# ── assert real repo untouched ───────────────────────────────────────────
|
|
1052
|
+
|
|
1053
|
+
test_tests_dont_touch_real_repo() {
|
|
1054
|
+
# This test verifies that our test harness (make_repo) creates isolated
|
|
1055
|
+
# temp repos and doesn't affect the real repository.
|
|
1056
|
+
# We just check that PROJECT_ROOT is different from TEST_REPO.
|
|
1057
|
+
make_repo
|
|
1058
|
+
trap cleanup_repo RETURN
|
|
1059
|
+
|
|
1060
|
+
if [ "$TEST_REPO" != "$PROJECT_ROOT" ]; then
|
|
1061
|
+
pass "tests use isolated temp repos, not PROJECT_ROOT"
|
|
1062
|
+
else
|
|
1063
|
+
fail "isolated temp repos" "TEST_REPO == PROJECT_ROOT"
|
|
1064
|
+
fi
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
# ── sprint status ahead/behind tests ─────────────────────────────────────
|
|
1068
|
+
|
|
1069
|
+
test_sprint_status_ahead_behind() {
|
|
1070
|
+
# sprint status must compare against main, not HEAD.
|
|
1071
|
+
# After 2 commits on sprint, ahead should be 2.
|
|
1072
|
+
make_repo
|
|
1073
|
+
trap cleanup_repo RETURN
|
|
1074
|
+
|
|
1075
|
+
echo "initial" > file.txt
|
|
1076
|
+
git add -A && git commit -q -m "initial"
|
|
1077
|
+
git branch -m main 2>/dev/null || true
|
|
1078
|
+
|
|
1079
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
1080
|
+
|
|
1081
|
+
# Two commits on sprint branch
|
|
1082
|
+
echo "sprint 1" > s1.txt
|
|
1083
|
+
git add -A && git commit -q -m "sprint commit 1"
|
|
1084
|
+
echo "sprint 2" > s2.txt
|
|
1085
|
+
git add -A && git commit -q -m "sprint commit 2"
|
|
1086
|
+
|
|
1087
|
+
local out
|
|
1088
|
+
out="$(bash scripts/worktree.sh sprint status)"
|
|
1089
|
+
if printf '%s\n' "$out" | grep -q "ahead: 2 behind: 0"; then
|
|
1090
|
+
pass "sprint status shows ahead: 2 behind: 0"
|
|
1091
|
+
else
|
|
1092
|
+
fail "sprint status ahead/behind" "output: $out"
|
|
1093
|
+
fi
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
test_sprint_status_ahead_zero_at_start() {
|
|
1097
|
+
# Right after sprint start (no new commits), ahead should be 0.
|
|
1098
|
+
make_repo
|
|
1099
|
+
trap cleanup_repo RETURN
|
|
1100
|
+
|
|
1101
|
+
echo "initial" > file.txt
|
|
1102
|
+
git add -A && git commit -q -m "initial"
|
|
1103
|
+
git branch -m main 2>/dev/null || true
|
|
1104
|
+
|
|
1105
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
1106
|
+
|
|
1107
|
+
local out
|
|
1108
|
+
out="$(bash scripts/worktree.sh sprint status)"
|
|
1109
|
+
if printf '%s\n' "$out" | grep -q "ahead: 0 behind: 0"; then
|
|
1110
|
+
pass "sprint status shows ahead: 0 behind: 0 at start"
|
|
1111
|
+
else
|
|
1112
|
+
fail "sprint status at start" "output: $out"
|
|
1113
|
+
fi
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
# ── mr command tests ─────────────────────────────────────────────────────
|
|
1117
|
+
|
|
1118
|
+
# Helper: set up a repo with sprint + task worktree for mr/merge tests.
|
|
1119
|
+
# Creates: main branch, sprint/1-test, task/<name> worktree with a commit.
|
|
1120
|
+
# NOTE: Does NOT set trap — the caller must do `trap cleanup_repo RETURN`.
|
|
1121
|
+
setup_mr_repo() {
|
|
1122
|
+
local task_name="$1"
|
|
1123
|
+
make_repo
|
|
1124
|
+
|
|
1125
|
+
echo "initial" > file.txt
|
|
1126
|
+
git add -A && git commit -q -m "initial"
|
|
1127
|
+
git branch -m main 2>/dev/null || true
|
|
1128
|
+
|
|
1129
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
1130
|
+
|
|
1131
|
+
# Make a commit on sprint so it diverges from main
|
|
1132
|
+
echo "sprint work" > sprint-file.txt
|
|
1133
|
+
git add -A && git commit -q -m "sprint work"
|
|
1134
|
+
|
|
1135
|
+
# Create task worktree from sprint HEAD
|
|
1136
|
+
bash scripts/worktree.sh new "$task_name" --task-id 1.1 >/dev/null
|
|
1137
|
+
|
|
1138
|
+
# Make a commit in the task worktree
|
|
1139
|
+
local wt
|
|
1140
|
+
wt="$(bash scripts/worktree.sh path "$task_name")"
|
|
1141
|
+
echo "task work" > "$wt/task-file.txt"
|
|
1142
|
+
git -C "$wt" add -A
|
|
1143
|
+
git -C "$wt" commit -q -m "task work"
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
test_run_requires_a_spec() {
|
|
1147
|
+
make_repo
|
|
1148
|
+
trap cleanup_repo RETURN
|
|
1149
|
+
local rc=0
|
|
1150
|
+
bash scripts/worktree.sh run foo >/dev/null 2>&1 || rc=$?
|
|
1151
|
+
if [ "$rc" -eq 1 ]; then
|
|
1152
|
+
pass "run without --file exits 1 (usage)"
|
|
1153
|
+
else
|
|
1154
|
+
fail "run usage" "expected exit 1, got $rc"
|
|
1155
|
+
fi
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
test_retry_usage_and_limit() {
|
|
1159
|
+
make_repo
|
|
1160
|
+
trap cleanup_repo RETURN
|
|
1161
|
+
printf 'fix\n' > fixes.md
|
|
1162
|
+
local rc=0
|
|
1163
|
+
|
|
1164
|
+
bash scripts/worktree.sh retry x >/dev/null 2>&1 || rc=$?
|
|
1165
|
+
if [ "$rc" -eq 1 ]; then pass "retry without --fixes exits 1"; else fail "retry usage" "got $rc"; fi
|
|
1166
|
+
|
|
1167
|
+
rc=0
|
|
1168
|
+
bash scripts/worktree.sh retry y --fixes fixes.md >/dev/null 2>&1 || rc=$?
|
|
1169
|
+
if [ "$rc" -eq 1 ]; then pass "retry without a worktree exits 1"; else fail "retry no worktree" "got $rc"; fi
|
|
1170
|
+
|
|
1171
|
+
mkdir -p tmp/worktrees/rl tmp/meta
|
|
1172
|
+
echo 2 > tmp/meta/rl.retries
|
|
1173
|
+
rc=0
|
|
1174
|
+
bash scripts/worktree.sh retry rl --fixes fixes.md >/dev/null 2>&1 || rc=$?
|
|
1175
|
+
if [ "$rc" -eq 1 ]; then pass "retry over the limit (max 2) exits 1"; else fail "retry limit" "got $rc"; fi
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
test_sprint_run_dry_run() {
|
|
1179
|
+
make_repo
|
|
1180
|
+
trap cleanup_repo RETURN
|
|
1181
|
+
printf 'import sys\nprint("no disjoint ready tasks")\n' > scripts/tasks.py
|
|
1182
|
+
git add -A && git commit -q -m base
|
|
1183
|
+
local rc=0
|
|
1184
|
+
bash scripts/worktree.sh sprint run --dry-run >/dev/null 2>&1 || rc=$?
|
|
1185
|
+
if [ "$rc" -eq 0 ]; then
|
|
1186
|
+
pass "sprint run --dry-run exits 0"
|
|
1187
|
+
else
|
|
1188
|
+
fail "sprint run dry-run" "rc=$rc"
|
|
1189
|
+
fi
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
test_sprint_run_reports_results_and_planner_failure() {
|
|
1193
|
+
make_repo
|
|
1194
|
+
trap cleanup_repo RETURN
|
|
1195
|
+
# Stub tracker: plan-batch offers one task that has no spec link; `show` returns no links.
|
|
1196
|
+
cat > scripts/tasks.py <<'PY'
|
|
1197
|
+
import sys
|
|
1198
|
+
if sys.argv[1] == "plan-batch":
|
|
1199
|
+
print("9.9 Needs a spec")
|
|
1200
|
+
elif sys.argv[1] == "show":
|
|
1201
|
+
print("{}")
|
|
1202
|
+
PY
|
|
1203
|
+
git add -A && git commit -q -m base
|
|
1204
|
+
local out rc=0
|
|
1205
|
+
out="$(bash scripts/worktree.sh sprint run 2>&1)" || rc=$?
|
|
1206
|
+
if [ "$rc" -eq 0 ] && printf '%s\n' "$out" | grep -q "9.9 skipped (no spec link)" \
|
|
1207
|
+
&& printf '%s\n' "$out" | grep -q "(none)"; then
|
|
1208
|
+
pass "sprint run reports skipped tasks and an empty MR list"
|
|
1209
|
+
else
|
|
1210
|
+
fail "sprint run results" "rc=$rc output: $out"
|
|
1211
|
+
fi
|
|
1212
|
+
|
|
1213
|
+
printf 'import sys\nsys.exit(2)\n' > scripts/tasks.py
|
|
1214
|
+
rc=0
|
|
1215
|
+
bash scripts/worktree.sh sprint run >/dev/null 2>&1 || rc=$?
|
|
1216
|
+
if [ "$rc" -eq 1 ]; then pass "sprint run exits 1 when plan-batch fails"; else fail "sprint run planner failure" "rc=$rc"; fi
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
test_sprint_run_reports_failed_task_without_stale_mrs() {
|
|
1220
|
+
make_repo
|
|
1221
|
+
trap cleanup_repo RETURN
|
|
1222
|
+
mkdir -p tmp/tasks tmp/mr
|
|
1223
|
+
printf 'spec\n' > tmp/tasks/failed.md
|
|
1224
|
+
printf 'old review\n' > tmp/mr/failed.md
|
|
1225
|
+
cat > scripts/tasks.py <<'PY'
|
|
1226
|
+
import sys
|
|
1227
|
+
if sys.argv[1] == "plan-batch":
|
|
1228
|
+
print("9.9 Failed task")
|
|
1229
|
+
elif sys.argv[1] == "show":
|
|
1230
|
+
print('{"links": [{"kind": "spec", "value": "tmp/tasks/failed.md"}]}')
|
|
1231
|
+
PY
|
|
1232
|
+
printf 'exit 7\n' > scripts/run-task.sh
|
|
1233
|
+
git add -A && git commit -q -m base
|
|
1234
|
+
|
|
1235
|
+
local out rc=0
|
|
1236
|
+
out="$(bash scripts/worktree.sh sprint run 2>&1)" || rc=$?
|
|
1237
|
+
if [ "$rc" -eq 1 ] && printf '%s\n' "$out" | grep -q "9.9 failed exit 7" \
|
|
1238
|
+
&& printf '%s\n' "$out" | grep -q "1 task(s) failed" \
|
|
1239
|
+
&& printf '%s\n' "$out" | grep -q "MRs awaiting review:" \
|
|
1240
|
+
&& printf '%s\n' "$out" | grep -q "(none)" \
|
|
1241
|
+
&& ! printf '%s\n' "$out" | grep -q "failed.md"; then
|
|
1242
|
+
pass "sprint run exits 1 for a failed task and excludes its stale MR"
|
|
1243
|
+
else
|
|
1244
|
+
fail "sprint run failed task summary" "rc=$rc output: $out"
|
|
1245
|
+
fi
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
test_sprint_run_lists_only_mrs_created_by_run() {
|
|
1249
|
+
setup_mr_repo "run-only"
|
|
1250
|
+
trap cleanup_repo RETURN
|
|
1251
|
+
mkdir -p tmp/tasks tmp/mr
|
|
1252
|
+
printf 'spec\n' > tmp/tasks/run-only.md
|
|
1253
|
+
printf 'old review\n' > tmp/mr/stale.md
|
|
1254
|
+
cat > scripts/tasks.py <<'PY'
|
|
1255
|
+
import sys
|
|
1256
|
+
if sys.argv[1] == "plan-batch":
|
|
1257
|
+
print("1.1 Run task")
|
|
1258
|
+
elif sys.argv[1] == "show":
|
|
1259
|
+
print('{"links": [{"kind": "spec", "value": "tmp/tasks/run-only.md"}]}')
|
|
1260
|
+
PY
|
|
1261
|
+
printf 'exit 0\n' > scripts/run-task.sh
|
|
1262
|
+
|
|
1263
|
+
local out rc=0 mr_section
|
|
1264
|
+
out="$(ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh sprint run 2>&1)" || rc=$?
|
|
1265
|
+
mr_section="${out#*MRs awaiting review:}"
|
|
1266
|
+
if [ "$rc" -eq 0 ] && printf '%s\n' "$out" | grep -q "1.1 run-only exit 0" \
|
|
1267
|
+
&& printf '%s\n' "$mr_section" | grep -q "run-only" \
|
|
1268
|
+
&& ! printf '%s\n' "$mr_section" | grep -q "stale"; then
|
|
1269
|
+
pass "sprint run lists only MRs created by this run"
|
|
1270
|
+
else
|
|
1271
|
+
fail "sprint run MR scope" "rc=$rc output: $out"
|
|
1272
|
+
fi
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
test_accept_requires_a_review_marker() {
|
|
1276
|
+
make_repo
|
|
1277
|
+
trap cleanup_repo RETURN
|
|
1278
|
+
mkdir -p tmp/meta
|
|
1279
|
+
local rc=0
|
|
1280
|
+
|
|
1281
|
+
bash scripts/worktree.sh accept nometa >/dev/null 2>&1 || rc=$?
|
|
1282
|
+
if [ "$rc" -eq 1 ]; then pass "accept without a task id exits 1"; else fail "accept task id" "got $rc"; fi
|
|
1283
|
+
|
|
1284
|
+
echo 1.1 > tmp/meta/ac.task
|
|
1285
|
+
rc=0
|
|
1286
|
+
bash scripts/worktree.sh accept ac >/dev/null 2>&1 || rc=$?
|
|
1287
|
+
if [ "$rc" -eq 6 ]; then pass "accept without an MR marker exits 6"; else fail "accept marker" "got $rc"; fi
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
test_accept_fails_loudly_when_links_cannot_be_recorded() {
|
|
1291
|
+
setup_mr_repo "acc-links"
|
|
1292
|
+
trap cleanup_repo RETURN
|
|
1293
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr acc-links >/dev/null 2>&1
|
|
1294
|
+
# Task 1.1 does not exist in this fixture's tracker, so every `link` call fails.
|
|
1295
|
+
local out rc=0
|
|
1296
|
+
out="$(bash scripts/worktree.sh accept acc-links 2>&1)" || rc=$?
|
|
1297
|
+
if [ "$rc" -eq 13 ] && printf '%s\n' "$out" | grep -q "could not record links"; then
|
|
1298
|
+
pass "accept: failed link recording exits 13"
|
|
1299
|
+
else
|
|
1300
|
+
fail "accept link failure" "rc=$rc output: $out"
|
|
1301
|
+
fi
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
test_accept_links_only_retries_the_links() {
|
|
1305
|
+
setup_mr_repo "acc-retry"
|
|
1306
|
+
trap cleanup_repo RETURN
|
|
1307
|
+
# Stub tracker that records every call; `link` succeeds only when LINK_OK=1.
|
|
1308
|
+
cat > scripts/tasks.py <<'PY'
|
|
1309
|
+
import os, sys
|
|
1310
|
+
open(os.environ["TRACKER_LOG"], "a").write(" ".join(sys.argv[1:]) + "\n")
|
|
1311
|
+
sys.exit(0 if sys.argv[1] != "link" or os.environ.get("LINK_OK") == "1" else 1)
|
|
1312
|
+
PY
|
|
1313
|
+
git add -A && git commit -q -m "stub tracker"
|
|
1314
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr acc-retry >/dev/null 2>&1
|
|
1315
|
+
export TRACKER_LOG="$PWD/tracker.log"
|
|
1316
|
+
local rc=0 out
|
|
1317
|
+
out="$(LINK_OK=0 bash scripts/worktree.sh accept acc-retry 2>&1)" || rc=$?
|
|
1318
|
+
if [ "$rc" -eq 13 ] && printf '%s\n' "$out" | grep -q "retry: .* accept acc-retry --links-only --task-id 1.1"; then
|
|
1319
|
+
pass "accept: exit 13 prints the --links-only retry command"
|
|
1320
|
+
else
|
|
1321
|
+
fail "accept retry hint" "rc=$rc output: $out"
|
|
1322
|
+
fi
|
|
1323
|
+
|
|
1324
|
+
rc=0
|
|
1325
|
+
bash scripts/worktree.sh accept acc-retry --links-only >/dev/null 2>&1 || rc=$?
|
|
1326
|
+
[ "$rc" -eq 1 ] && pass "accept --links-only without --task-id exits 1" || fail "links-only needs id" "rc=$rc"
|
|
1327
|
+
|
|
1328
|
+
: > "$TRACKER_LOG"
|
|
1329
|
+
rc=0
|
|
1330
|
+
LINK_OK=1 bash scripts/worktree.sh accept acc-retry --links-only --task-id 1.1 >/dev/null 2>&1 || rc=$?
|
|
1331
|
+
if [ "$rc" -eq 0 ] && grep -q "^link 1.1 commit " "$TRACKER_LOG" && ! grep -q "^done" "$TRACKER_LOG"; then
|
|
1332
|
+
pass "accept --links-only records the links without merging"
|
|
1333
|
+
else
|
|
1334
|
+
fail "links-only" "rc=$rc log: $(cat "$TRACKER_LOG")"
|
|
1335
|
+
fi
|
|
1336
|
+
unset TRACKER_LOG
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
test_mr_happy_path() {
|
|
1340
|
+
setup_mr_repo "mr-test"
|
|
1341
|
+
trap cleanup_repo RETURN
|
|
1342
|
+
|
|
1343
|
+
local out rc=0
|
|
1344
|
+
out="$(ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr mr-test 2>&1)" || rc=$?
|
|
1345
|
+
if [ "$rc" -eq 0 ]; then
|
|
1346
|
+
if [ -f "tmp/mr/mr-test.md" ]; then
|
|
1347
|
+
local mr_content
|
|
1348
|
+
mr_content="$(cat tmp/mr/mr-test.md)"
|
|
1349
|
+
if printf '%s\n' "$mr_content" | grep -q "# MR: mr-test" && \
|
|
1350
|
+
printf '%s\n' "$mr_content" | grep -q "1.1" && \
|
|
1351
|
+
printf '%s\n' "$mr_content" | grep -q "passed" && \
|
|
1352
|
+
printf '%s\n' "$mr_content" | grep -q "Diffstat"; then
|
|
1353
|
+
pass "mr happy path creates MR record"
|
|
1354
|
+
else
|
|
1355
|
+
fail "mr record content" "$mr_content"
|
|
1356
|
+
fi
|
|
1357
|
+
else
|
|
1358
|
+
fail "mr record file" "tmp/mr/mr-test.md not found"
|
|
1359
|
+
fi
|
|
1360
|
+
else
|
|
1361
|
+
fail "mr happy path" "rc=$rc output: $out"
|
|
1362
|
+
fi
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
test_mr_with_remote_flag() {
|
|
1366
|
+
setup_mr_repo "mr-remote"
|
|
1367
|
+
trap cleanup_repo RETURN
|
|
1368
|
+
|
|
1369
|
+
local out rc=0
|
|
1370
|
+
out="$(ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr mr-remote --remote 2>&1)" || rc=$?
|
|
1371
|
+
if [ "$rc" -eq 0 ] && [ -f "tmp/mr/mr-remote.md" ]; then
|
|
1372
|
+
local mr_content
|
|
1373
|
+
mr_content="$(cat tmp/mr/mr-remote.md)"
|
|
1374
|
+
if printf '%s\n' "$mr_content" | grep -q "Remote hint"; then
|
|
1375
|
+
pass "mr --remote adds remote hint"
|
|
1376
|
+
else
|
|
1377
|
+
fail "mr --remote hint" "$mr_content"
|
|
1378
|
+
fi
|
|
1379
|
+
else
|
|
1380
|
+
fail "mr --remote" "rc=$rc"
|
|
1381
|
+
fi
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
test_mr_no_worktree() {
|
|
1385
|
+
make_repo
|
|
1386
|
+
trap cleanup_repo RETURN
|
|
1387
|
+
|
|
1388
|
+
echo "initial" > file.txt
|
|
1389
|
+
git add -A && git commit -q -m "initial"
|
|
1390
|
+
|
|
1391
|
+
local rc=0
|
|
1392
|
+
bash scripts/worktree.sh mr nonexistent >/dev/null 2>&1 || rc=$?
|
|
1393
|
+
if [ "$rc" -eq 1 ]; then
|
|
1394
|
+
pass "mr exits 1 when worktree missing"
|
|
1395
|
+
else
|
|
1396
|
+
fail "mr no worktree" "expected exit 1, got $rc"
|
|
1397
|
+
fi
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
test_mr_no_sprint_meta() {
|
|
1401
|
+
make_repo
|
|
1402
|
+
trap cleanup_repo RETURN
|
|
1403
|
+
|
|
1404
|
+
echo "initial" > file.txt
|
|
1405
|
+
git add -A && git commit -q -m "initial"
|
|
1406
|
+
mkdir -p tmp/worktrees/no-meta
|
|
1407
|
+
# No sprint meta file
|
|
1408
|
+
|
|
1409
|
+
local rc=0
|
|
1410
|
+
bash scripts/worktree.sh mr no-meta >/dev/null 2>&1 || rc=$?
|
|
1411
|
+
if [ "$rc" -eq 1 ]; then
|
|
1412
|
+
pass "mr exits 1 when sprint meta missing"
|
|
1413
|
+
else
|
|
1414
|
+
fail "mr no sprint meta" "expected exit 1, got $rc"
|
|
1415
|
+
fi
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
test_mr_merge_conflict_exit9() {
|
|
1419
|
+
setup_mr_repo "mr-conflict"
|
|
1420
|
+
trap cleanup_repo RETURN
|
|
1421
|
+
|
|
1422
|
+
local wt
|
|
1423
|
+
wt="$(bash scripts/worktree.sh path "mr-conflict")"
|
|
1424
|
+
|
|
1425
|
+
# Create a conflicting commit on the sprint branch (same file, different content)
|
|
1426
|
+
echo "sprint changed task-file" > task-file.txt
|
|
1427
|
+
git add -A && git commit -q -m "sprint changes task-file"
|
|
1428
|
+
|
|
1429
|
+
# Now the task branch has "task work" and sprint has "sprint changed task-file"
|
|
1430
|
+
# Merging sprint into task should conflict
|
|
1431
|
+
local rc=0
|
|
1432
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr mr-conflict >/dev/null 2>&1 || rc=$?
|
|
1433
|
+
if [ "$rc" -eq 9 ]; then
|
|
1434
|
+
pass "mr exits 9 on conflict"
|
|
1435
|
+
else
|
|
1436
|
+
fail "mr conflict" "expected exit 9, got $rc"
|
|
1437
|
+
fi
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
test_mr_checks_fail_exit8() {
|
|
1441
|
+
setup_mr_repo "mr-checks"
|
|
1442
|
+
trap cleanup_repo RETURN
|
|
1443
|
+
|
|
1444
|
+
local rc=0
|
|
1445
|
+
ACCEPT_CHECKS_CMD='exit 1' bash scripts/worktree.sh mr mr-checks >/dev/null 2>&1 || rc=$?
|
|
1446
|
+
if [ "$rc" -eq 8 ]; then
|
|
1447
|
+
pass "mr exits 8 when checks fail"
|
|
1448
|
+
else
|
|
1449
|
+
fail "mr checks fail" "expected exit 8, got $rc"
|
|
1450
|
+
fi
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
test_mr_path_guard_exit4() {
|
|
1454
|
+
setup_mr_repo "mr-guard"
|
|
1455
|
+
trap cleanup_repo RETURN
|
|
1456
|
+
|
|
1457
|
+
local wt
|
|
1458
|
+
wt="$(bash scripts/worktree.sh path "mr-guard")"
|
|
1459
|
+
|
|
1460
|
+
# Add a forbidden file (kilo.json) to the task branch
|
|
1461
|
+
echo "forbidden" > "$wt/kilo.json"
|
|
1462
|
+
git -C "$wt" add -A
|
|
1463
|
+
git -C "$wt" commit -q -m "add forbidden file"
|
|
1464
|
+
|
|
1465
|
+
local rc=0
|
|
1466
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr mr-guard >/dev/null 2>&1 || rc=$?
|
|
1467
|
+
if [ "$rc" -eq 4 ]; then
|
|
1468
|
+
pass "mr exits 4 on forbidden path"
|
|
1469
|
+
else
|
|
1470
|
+
fail "mr path guard" "expected exit 4, got $rc"
|
|
1471
|
+
fi
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
# ── merge command tests ──────────────────────────────────────────────────
|
|
1475
|
+
|
|
1476
|
+
test_merge_happy_path() {
|
|
1477
|
+
setup_mr_repo "merge-test"
|
|
1478
|
+
trap cleanup_repo RETURN
|
|
1479
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr merge-test >/dev/null 2>&1
|
|
1480
|
+
|
|
1481
|
+
local out rc=0
|
|
1482
|
+
out="$(bash scripts/worktree.sh merge merge-test 2>&1)" || rc=$?
|
|
1483
|
+
if [ "$rc" -eq 0 ]; then
|
|
1484
|
+
if printf '%s\n' "$out" | grep -q "merged task/merge-test into sprint/1-test"; then
|
|
1485
|
+
# Verify task branch was deleted
|
|
1486
|
+
local branch_exists=0
|
|
1487
|
+
git rev-parse --verify "task/merge-test" >/dev/null 2>&1 && branch_exists=1
|
|
1488
|
+
if [ "$branch_exists" -eq 0 ]; then
|
|
1489
|
+
pass "merge happy path merges and cleans up"
|
|
1490
|
+
else
|
|
1491
|
+
fail "merge cleanup" "task branch still exists"
|
|
1492
|
+
fi
|
|
1493
|
+
else
|
|
1494
|
+
fail "merge output" "output: $out"
|
|
1495
|
+
fi
|
|
1496
|
+
else
|
|
1497
|
+
fail "merge happy path" "rc=$rc output: $out"
|
|
1498
|
+
fi
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
test_merge_red_flags_require_accept_flags() {
|
|
1502
|
+
setup_mr_repo "merge-flags"
|
|
1503
|
+
trap cleanup_repo RETURN
|
|
1504
|
+
local wt
|
|
1505
|
+
wt="$(bash scripts/worktree.sh path merge-flags)"
|
|
1506
|
+
printf '#pragma warning disable SA0001\n' >> "$wt/task-file.txt"
|
|
1507
|
+
git -C "$wt" add -A && git -C "$wt" commit -q -m "1.1 add a pragma"
|
|
1508
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr merge-flags >/dev/null 2>&1
|
|
1509
|
+
|
|
1510
|
+
local rc=0
|
|
1511
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh merge merge-flags >/dev/null 2>&1 || rc=$?
|
|
1512
|
+
if [ "$rc" -eq 12 ]; then
|
|
1513
|
+
pass "merge: red flags refused without --accept-flags (exit 12)"
|
|
1514
|
+
else
|
|
1515
|
+
fail "merge red-flag gate" "rc=$rc"
|
|
1516
|
+
return
|
|
1517
|
+
fi
|
|
1518
|
+
|
|
1519
|
+
rc=0
|
|
1520
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh merge merge-flags --accept-flags >/dev/null 2>&1 || rc=$?
|
|
1521
|
+
if [ "$rc" -eq 0 ]; then
|
|
1522
|
+
pass "merge: --accept-flags accepts the red flags"
|
|
1523
|
+
else
|
|
1524
|
+
fail "merge accept-flags" "rc=$rc"
|
|
1525
|
+
fi
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
test_merge_auto_builds_mr() {
|
|
1529
|
+
setup_mr_repo "merge-auto"
|
|
1530
|
+
trap cleanup_repo RETURN
|
|
1531
|
+
# No `mr` first: `merge --auto` must build the MR (checks overridden green) then merge.
|
|
1532
|
+
local out rc=0
|
|
1533
|
+
out="$(ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh merge merge-auto --auto 2>&1)" || rc=$?
|
|
1534
|
+
if [ "$rc" -eq 0 ] && printf '%s\n' "$out" | grep -q "merged task/merge-auto into sprint/1-test"; then
|
|
1535
|
+
pass "merge --auto builds the MR and merges on green"
|
|
1536
|
+
else
|
|
1537
|
+
fail "merge --auto" "rc=$rc out=$out"
|
|
1538
|
+
fi
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
test_needs_dotnet_checks() {
|
|
1542
|
+
make_repo
|
|
1543
|
+
trap cleanup_repo RETURN
|
|
1544
|
+
git add -A && git commit -q -m "base"
|
|
1545
|
+
local base base2
|
|
1546
|
+
base="$(git rev-parse HEAD)"
|
|
1547
|
+
echo "class A {}" > a.cs
|
|
1548
|
+
git add -A && git commit -q -m "add cs"
|
|
1549
|
+
if bash scripts/needs-dotnet-checks.sh . "$base" HEAD; then
|
|
1550
|
+
pass "needs-dotnet-checks: a .cs change needs dotnet"
|
|
1551
|
+
else
|
|
1552
|
+
fail "needs-dotnet-checks .cs" "expected exit 0"
|
|
1553
|
+
fi
|
|
1554
|
+
base2="$(git rev-parse HEAD)"
|
|
1555
|
+
echo "note" >> README.md
|
|
1556
|
+
git add -A && git commit -q -m "docs"
|
|
1557
|
+
if bash scripts/needs-dotnet-checks.sh . "$base2" HEAD; then
|
|
1558
|
+
fail "needs-dotnet-checks docs" "expected exit 1"
|
|
1559
|
+
else
|
|
1560
|
+
pass "needs-dotnet-checks: a docs-only change skips dotnet"
|
|
1561
|
+
fi
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
test_merge_no_worktree() {
|
|
1565
|
+
make_repo
|
|
1566
|
+
trap cleanup_repo RETURN
|
|
1567
|
+
|
|
1568
|
+
echo "initial" > file.txt
|
|
1569
|
+
git add -A && git commit -q -m "initial"
|
|
1570
|
+
|
|
1571
|
+
local rc=0
|
|
1572
|
+
bash scripts/worktree.sh merge nonexistent >/dev/null 2>&1 || rc=$?
|
|
1573
|
+
if [ "$rc" -eq 1 ]; then
|
|
1574
|
+
pass "merge exits 1 when worktree missing"
|
|
1575
|
+
else
|
|
1576
|
+
fail "merge no worktree" "expected exit 1, got $rc"
|
|
1577
|
+
fi
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
test_merge_no_sprint_meta() {
|
|
1581
|
+
make_repo
|
|
1582
|
+
trap cleanup_repo RETURN
|
|
1583
|
+
|
|
1584
|
+
echo "initial" > file.txt
|
|
1585
|
+
git add -A && git commit -q -m "initial"
|
|
1586
|
+
mkdir -p tmp/worktrees/no-meta
|
|
1587
|
+
|
|
1588
|
+
local rc=0
|
|
1589
|
+
bash scripts/worktree.sh merge no-meta >/dev/null 2>&1 || rc=$?
|
|
1590
|
+
if [ "$rc" -eq 1 ]; then
|
|
1591
|
+
pass "merge exits 1 when sprint meta missing"
|
|
1592
|
+
else
|
|
1593
|
+
fail "merge no sprint meta" "expected exit 1, got $rc"
|
|
1594
|
+
fi
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
test_merge_sprint_branch_missing_exit6() {
|
|
1598
|
+
setup_mr_repo "merge-no-sprint"
|
|
1599
|
+
trap cleanup_repo RETURN
|
|
1600
|
+
|
|
1601
|
+
# Switch to main first so we can delete the sprint branch
|
|
1602
|
+
git checkout -q main
|
|
1603
|
+
git branch -D "sprint/1-test" >/dev/null 2>&1
|
|
1604
|
+
|
|
1605
|
+
local rc=0
|
|
1606
|
+
bash scripts/worktree.sh merge merge-no-sprint >/dev/null 2>&1 || rc=$?
|
|
1607
|
+
if [ "$rc" -eq 6 ]; then
|
|
1608
|
+
pass "merge exits 6 when sprint branch missing"
|
|
1609
|
+
else
|
|
1610
|
+
fail "merge sprint missing" "expected exit 6, got $rc"
|
|
1611
|
+
fi
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
test_merge_stale_task_exit6() {
|
|
1615
|
+
setup_mr_repo "merge-stale"
|
|
1616
|
+
trap cleanup_repo RETURN
|
|
1617
|
+
|
|
1618
|
+
# MR is built, then the sprint branch moves on: the tested merge is no longer the real one.
|
|
1619
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr merge-stale >/dev/null 2>&1
|
|
1620
|
+
echo "later sprint work" > later.txt
|
|
1621
|
+
git add -A && git commit -q -m "sprint moved after the MR"
|
|
1622
|
+
|
|
1623
|
+
local rc=0
|
|
1624
|
+
bash scripts/worktree.sh merge merge-stale >/dev/null 2>&1 || rc=$?
|
|
1625
|
+
if [ "$rc" -eq 6 ]; then
|
|
1626
|
+
pass "merge exits 6 when the sprint branch moved after the MR was built"
|
|
1627
|
+
else
|
|
1628
|
+
fail "merge stale" "expected exit 6, got $rc"
|
|
1629
|
+
fi
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
test_merge_without_mr_record_exit6() {
|
|
1633
|
+
setup_mr_repo "merge-nomr"
|
|
1634
|
+
trap cleanup_repo RETURN
|
|
1635
|
+
local rc=0
|
|
1636
|
+
bash scripts/worktree.sh merge merge-nomr >/dev/null 2>&1 || rc=$?
|
|
1637
|
+
if [ "$rc" -eq 6 ] && ! git log --merges --oneline | grep -q .; then
|
|
1638
|
+
pass "merge exits 6 without an MR record (checks cannot be skipped), nothing merged"
|
|
1639
|
+
else
|
|
1640
|
+
fail "merge without MR" "expected exit 6, got $rc"
|
|
1641
|
+
fi
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
test_merge_real_trailers_and_dirty_tree() {
|
|
1645
|
+
setup_mr_repo "merge-trl"
|
|
1646
|
+
trap cleanup_repo RETURN
|
|
1647
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr merge-trl >/dev/null 2>&1
|
|
1648
|
+
echo "uncommitted" > dirty.txt
|
|
1649
|
+
local rc=0
|
|
1650
|
+
bash scripts/worktree.sh merge merge-trl >/dev/null 2>&1 || rc=$?
|
|
1651
|
+
if [ "$rc" -eq 1 ]; then pass "merge refuses a dirty main tree (exit 1)"; else fail "merge dirty tree" "expected 1, got $rc"; fi
|
|
1652
|
+
rm dirty.txt
|
|
1653
|
+
bash scripts/worktree.sh merge merge-trl >/dev/null 2>&1 || { fail "merge trailers" "merge failed"; return; }
|
|
1654
|
+
local trailers
|
|
1655
|
+
trailers="$(git log -1 --format='%(trailers:only,unfold)')"
|
|
1656
|
+
if printf '%s' "$trailers" | grep -q "^Task: 1.1" && printf '%s' "$trailers" | grep -q "^Branch: task/merge-trl"; then
|
|
1657
|
+
pass "merge commit carries real git trailers (Task, Branch)"
|
|
1658
|
+
else
|
|
1659
|
+
fail "merge trailers" "trailers: $trailers"
|
|
1660
|
+
fi
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
test_merge_conflict_exit10() {
|
|
1664
|
+
setup_mr_repo "merge-conflict"
|
|
1665
|
+
trap cleanup_repo RETURN
|
|
1666
|
+
|
|
1667
|
+
local wt
|
|
1668
|
+
wt="$(bash scripts/worktree.sh path "merge-conflict")"
|
|
1669
|
+
|
|
1670
|
+
# Create a conflicting file on the sprint branch
|
|
1671
|
+
echo "sprint version" > conflict-file.txt
|
|
1672
|
+
git add -A && git commit -q -m "sprint conflict file"
|
|
1673
|
+
|
|
1674
|
+
# Create the same file with different content in the task worktree
|
|
1675
|
+
echo "task version" > "$wt/conflict-file.txt"
|
|
1676
|
+
git -C "$wt" add -A
|
|
1677
|
+
git -C "$wt" commit -q -m "task conflict file"
|
|
1678
|
+
|
|
1679
|
+
# With the MR gate a conflict surfaces in `mr` (exit 9, merge aborted), never in `merge`.
|
|
1680
|
+
local rc=0
|
|
1681
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr merge-conflict >/dev/null 2>&1 || rc=$?
|
|
1682
|
+
if [ "$rc" -eq 9 ]; then
|
|
1683
|
+
pass "conflict is caught by mr (exit 9)"
|
|
1684
|
+
else
|
|
1685
|
+
fail "mr conflict" "expected exit 9, got $rc"
|
|
1686
|
+
fi
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
test_merge_with_accept_flags() {
|
|
1690
|
+
setup_mr_repo "merge-flags"
|
|
1691
|
+
trap cleanup_repo RETURN
|
|
1692
|
+
ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr merge-flags >/dev/null 2>&1
|
|
1693
|
+
|
|
1694
|
+
local out rc=0
|
|
1695
|
+
out="$(bash scripts/worktree.sh merge merge-flags --accept-flags --some-flag 2>&1)" || rc=$?
|
|
1696
|
+
if [ "$rc" -eq 0 ]; then
|
|
1697
|
+
pass "merge accepts --accept-flags without error"
|
|
1698
|
+
else
|
|
1699
|
+
fail "merge --accept-flags" "rc=$rc output: $out"
|
|
1700
|
+
fi
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
# ── apply removed tests ──────────────────────────────────────────────────
|
|
1704
|
+
|
|
1705
|
+
test_apply_command_removed() {
|
|
1706
|
+
# The 'apply' command should no longer exist.
|
|
1707
|
+
make_repo
|
|
1708
|
+
trap cleanup_repo RETURN
|
|
1709
|
+
|
|
1710
|
+
echo "initial" > file.txt
|
|
1711
|
+
git add -A && git commit -q -m "initial"
|
|
1712
|
+
|
|
1713
|
+
local rc=0
|
|
1714
|
+
bash scripts/worktree.sh apply some-name >/dev/null 2>&1 || rc=$?
|
|
1715
|
+
# Should exit with code 2 (usage error — unknown command)
|
|
1716
|
+
if [ "$rc" -eq 2 ]; then
|
|
1717
|
+
pass "apply command removed (exit 2)"
|
|
1718
|
+
else
|
|
1719
|
+
fail "apply removed" "expected exit 2, got $rc"
|
|
1720
|
+
fi
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
test_patch_command_still_works() {
|
|
1724
|
+
# 'patch' should still work as a read-only export.
|
|
1725
|
+
make_repo
|
|
1726
|
+
trap cleanup_repo RETURN
|
|
1727
|
+
|
|
1728
|
+
echo "initial" > file.txt
|
|
1729
|
+
git add -A && git commit -q -m "initial"
|
|
1730
|
+
git branch -m main 2>/dev/null || true
|
|
1731
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
1732
|
+
bash scripts/worktree.sh new patch-test >/dev/null
|
|
1733
|
+
|
|
1734
|
+
local wt
|
|
1735
|
+
wt="$(bash scripts/worktree.sh path "patch-test")"
|
|
1736
|
+
echo "patched" > "$wt/patched.txt"
|
|
1737
|
+
git -C "$wt" add -A
|
|
1738
|
+
git -C "$wt" commit -q -m "patch commit"
|
|
1739
|
+
|
|
1740
|
+
local out rc=0
|
|
1741
|
+
out="$(bash scripts/worktree.sh patch patch-test 2>&1)" || rc=$?
|
|
1742
|
+
if [ "$rc" -eq 0 ] && [ -f "$TEST_REPO/tmp/patches/patch-test.patch" ]; then
|
|
1743
|
+
pass "patch command still works"
|
|
1744
|
+
else
|
|
1745
|
+
fail "patch command" "rc=$rc output: $out"
|
|
1746
|
+
fi
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
# ── main ─────────────────────────────────────────────────────────────────
|
|
1750
|
+
|
|
1751
|
+
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
1752
|
+
|
|
1753
|
+
# Source helper functions from worktree.sh (path_ok, is_generated_path, GENERATED_PATHS).
|
|
1754
|
+
# We cannot source worktree.sh directly (it has set -euo pipefail and a case statement).
|
|
1755
|
+
# Instead, we replicate the helper functions here for testing.
|
|
1756
|
+
GENERATED_PATHS="tasks/tasks.snapshot.json
|
|
1757
|
+
tasks/README.md
|
|
1758
|
+
tasks/sprint-*.md
|
|
1759
|
+
TASKS.md"
|
|
1760
|
+
|
|
1761
|
+
is_generated_path() {
|
|
1762
|
+
local p="$1"
|
|
1763
|
+
while IFS= read -r gp; do
|
|
1764
|
+
[[ "$p" == $gp ]] && return 0
|
|
1765
|
+
done <<< "$GENERATED_PATHS"
|
|
1766
|
+
return 1
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
path_ok() {
|
|
1770
|
+
local p="$1"
|
|
1771
|
+
[[ -n "$p" ]] || { echo "empty path" >&2; return 1; }
|
|
1772
|
+
if [[ "$p" =~ [^A-Za-z0-9._/\-] ]]; then
|
|
1773
|
+
echo "path '$p' contains forbidden characters" >&2; return 1
|
|
1774
|
+
fi
|
|
1775
|
+
[[ "${p:0:1}" != "/" ]] || { echo "path '$p' must not start with /" >&2; return 1; }
|
|
1776
|
+
local IFS='/'
|
|
1777
|
+
local -a segments
|
|
1778
|
+
read -ra segments <<< "$p"
|
|
1779
|
+
for seg in "${segments[@]}"; do
|
|
1780
|
+
[[ -n "$seg" ]] || { echo "path '$p' contains empty segment" >&2; return 1; }
|
|
1781
|
+
[[ "$seg" != ".." ]] || { echo "path '$p' contains '..' segment" >&2; return 1; }
|
|
1782
|
+
[[ "${seg:0:1}" != "-" ]] || { echo "path '$p' has segment starting with '-'" >&2; return 1; }
|
|
1783
|
+
done
|
|
1784
|
+
for dir in .git .githooks .claude .junie .kilo tmp; do
|
|
1785
|
+
if [[ "$p" == "$dir" || "$p" == "$dir/"* || "$p" == */"$dir" || "$p" == */"$dir/"* ]]; then
|
|
1786
|
+
echo "path '$p' is inside protected directory $dir/" >&2; return 1
|
|
1787
|
+
fi
|
|
1788
|
+
done
|
|
1789
|
+
case "$p" in
|
|
1790
|
+
kilo.json|.mcp.json|tasks/tasks.db|tasks/.lock)
|
|
1791
|
+
echo "path '$p' is a protected file" >&2; return 1 ;;
|
|
1792
|
+
esac
|
|
1793
|
+
if is_generated_path "$p"; then
|
|
1794
|
+
echo "path '$p' is a generated file" >&2; return 1
|
|
1795
|
+
fi
|
|
1796
|
+
return 0
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
root="$PROJECT_ROOT" # default root for path_ok tests
|
|
1800
|
+
|
|
1801
|
+
echo "=== worktree.sh core tests ==="
|
|
1802
|
+
|
|
1803
|
+
# Note: apply command tests removed — apply is retired; see 'apply removed tests' section
|
|
1804
|
+
|
|
1805
|
+
echo ""
|
|
1806
|
+
echo "=== path validation tests ==="
|
|
1807
|
+
|
|
1808
|
+
# Forbidden paths — each must exit 4
|
|
1809
|
+
test_forbidden_githooks_pre_commit
|
|
1810
|
+
test_forbidden_git_hooks
|
|
1811
|
+
test_forbidden_dotdot
|
|
1812
|
+
test_forbidden_absolute_path
|
|
1813
|
+
test_forbidden_space_in_name
|
|
1814
|
+
test_forbidden_non_ascii
|
|
1815
|
+
test_forbidden_dash_prefix
|
|
1816
|
+
test_forbidden_tmp
|
|
1817
|
+
test_forbidden_claude_settings
|
|
1818
|
+
test_forbidden_kilo_json
|
|
1819
|
+
test_forbidden_symlink_outside_scripts
|
|
1820
|
+
test_forbidden_double_slash
|
|
1821
|
+
test_forbidden_junie_dir
|
|
1822
|
+
test_forbidden_mcp_json
|
|
1823
|
+
test_forbidden_tasks_db
|
|
1824
|
+
test_forbidden_tasks_lock
|
|
1825
|
+
test_forbidden_kilo_dir
|
|
1826
|
+
|
|
1827
|
+
# Allowed paths — each must exit 0
|
|
1828
|
+
test_allowed_src_path
|
|
1829
|
+
test_allowed_scripts_path
|
|
1830
|
+
test_allowed_knowledge_path
|
|
1831
|
+
test_allowed_tests_path
|
|
1832
|
+
test_allowed_editorconfig
|
|
1833
|
+
|
|
1834
|
+
# Rename and delete of allowed paths
|
|
1835
|
+
test_rename_allowed_path
|
|
1836
|
+
test_delete_allowed_path
|
|
1837
|
+
|
|
1838
|
+
echo ""
|
|
1839
|
+
echo "=== sprint lifecycle tests ==="
|
|
1840
|
+
|
|
1841
|
+
test_sprint_start_creates_branch
|
|
1842
|
+
test_sprint_start_refuses_existing
|
|
1843
|
+
test_sprint_start_refuses_dirty_tree
|
|
1844
|
+
test_sprint_start_ignores_generated_paths
|
|
1845
|
+
test_sprint_status_on_sprint
|
|
1846
|
+
test_sprint_status_off_sprint
|
|
1847
|
+
test_sprint_finish_no_merge
|
|
1848
|
+
test_sprint_finish_merge
|
|
1849
|
+
test_sprint_finish_refuses_off_sprint
|
|
1850
|
+
|
|
1851
|
+
echo ""
|
|
1852
|
+
echo "=== new command tests (sprint-based) ==="
|
|
1853
|
+
|
|
1854
|
+
test_new_refuses_off_sprint
|
|
1855
|
+
test_new_refuses_dirty_tree
|
|
1856
|
+
test_new_ignores_generated_paths
|
|
1857
|
+
test_new_creates_task_branch_and_meta
|
|
1858
|
+
test_new_with_task_id
|
|
1859
|
+
test_new_baseline_is_sprint_head_not_carried
|
|
1860
|
+
test_new_refuses_existing_worktree
|
|
1861
|
+
|
|
1862
|
+
echo ""
|
|
1863
|
+
echo "=== GENERATED_PATHS tests ==="
|
|
1864
|
+
|
|
1865
|
+
test_generated_paths_forbidden_in_patch
|
|
1866
|
+
test_generated_paths_forbidden_view
|
|
1867
|
+
test_clean_tree_ignores_generated_snapshot
|
|
1868
|
+
test_clean_tree_ignores_generated_view
|
|
1869
|
+
|
|
1870
|
+
echo ""
|
|
1871
|
+
echo "=== abandon tests ==="
|
|
1872
|
+
|
|
1873
|
+
test_abandon_cleans_worktree_branch_meta
|
|
1874
|
+
test_abandon_idempotent
|
|
1875
|
+
|
|
1876
|
+
echo ""
|
|
1877
|
+
echo "=== remove compatibility tests ==="
|
|
1878
|
+
|
|
1879
|
+
test_remove_still_works
|
|
1880
|
+
|
|
1881
|
+
echo ""
|
|
1882
|
+
echo "=== isolation tests ==="
|
|
1883
|
+
|
|
1884
|
+
test_tests_dont_touch_real_repo
|
|
1885
|
+
|
|
1886
|
+
echo ""
|
|
1887
|
+
echo "=== sprint status ahead/behind tests ==="
|
|
1888
|
+
|
|
1889
|
+
test_sprint_status_ahead_behind
|
|
1890
|
+
test_sprint_status_ahead_zero_at_start
|
|
1891
|
+
|
|
1892
|
+
echo ""
|
|
1893
|
+
echo "=== mr command tests ==="
|
|
1894
|
+
|
|
1895
|
+
test_mr_happy_path
|
|
1896
|
+
test_run_requires_a_spec
|
|
1897
|
+
test_retry_usage_and_limit
|
|
1898
|
+
test_accept_requires_a_review_marker
|
|
1899
|
+
test_sprint_run_dry_run
|
|
1900
|
+
test_mr_with_remote_flag
|
|
1901
|
+
test_mr_no_worktree
|
|
1902
|
+
test_mr_no_sprint_meta
|
|
1903
|
+
test_mr_merge_conflict_exit9
|
|
1904
|
+
test_mr_checks_fail_exit8
|
|
1905
|
+
test_mr_path_guard_exit4
|
|
1906
|
+
|
|
1907
|
+
echo ""
|
|
1908
|
+
echo "=== merge command tests ==="
|
|
1909
|
+
|
|
1910
|
+
test_merge_happy_path
|
|
1911
|
+
test_merge_red_flags_require_accept_flags
|
|
1912
|
+
test_merge_auto_builds_mr
|
|
1913
|
+
test_needs_dotnet_checks
|
|
1914
|
+
test_merge_no_worktree
|
|
1915
|
+
test_merge_no_sprint_meta
|
|
1916
|
+
test_merge_sprint_branch_missing_exit6
|
|
1917
|
+
test_merge_stale_task_exit6
|
|
1918
|
+
test_merge_without_mr_record_exit6
|
|
1919
|
+
test_merge_real_trailers_and_dirty_tree
|
|
1920
|
+
test_merge_conflict_exit10
|
|
1921
|
+
test_merge_with_accept_flags
|
|
1922
|
+
|
|
1923
|
+
# ── commit command (runner commits on task/<name>) ───────────────────────
|
|
1924
|
+
|
|
1925
|
+
setup_commit_repo() {
|
|
1926
|
+
make_repo
|
|
1927
|
+
echo "initial" > file.txt
|
|
1928
|
+
git add -A && git commit -q -m "initial"
|
|
1929
|
+
git branch -m main 2>/dev/null || true
|
|
1930
|
+
bash scripts/worktree.sh sprint start 7 test >/dev/null
|
|
1931
|
+
bash scripts/worktree.sh new "$1" --task-id 7.7 >/dev/null
|
|
1932
|
+
CWT="$(bash scripts/worktree.sh path "$1")"
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
test_commit_identity_message_and_flow() {
|
|
1936
|
+
setup_commit_repo "c1"
|
|
1937
|
+
trap cleanup_repo RETURN
|
|
1938
|
+
echo "agent work" > "$CWT/new-file.txt"
|
|
1939
|
+
local rc=0 sha
|
|
1940
|
+
sha="$(bash scripts/worktree.sh commit c1 --runner junie --model junie-lite --task-id 7.7 --title "Do the thing")" || rc=$?
|
|
1941
|
+
local author msg
|
|
1942
|
+
author="$(git -C "$CWT" log -1 --format=%an)"
|
|
1943
|
+
msg="$(git -C "$CWT" log -1 --format=%B)"
|
|
1944
|
+
if [ "$rc" -eq 0 ] && [ "$author" = "agent:junie" ] && printf '%s' "$msg" | grep -q "^7.7 Do the thing" \
|
|
1945
|
+
&& printf '%s' "$msg" | grep -q "runner: junie" && printf '%s' "$msg" | grep -q "model: junie-lite" \
|
|
1946
|
+
&& [ "$sha" = "$(git -C "$CWT" rev-parse HEAD)" ]; then
|
|
1947
|
+
pass "commit: identity, message trailers and printed sha"
|
|
1948
|
+
else
|
|
1949
|
+
fail "commit identity/message" "rc=$rc author=$author msg=$msg"
|
|
1950
|
+
fi
|
|
1951
|
+
# end to end: mr then merge bring the agent's file into the sprint branch
|
|
1952
|
+
local out
|
|
1953
|
+
out="$(ACCEPT_CHECKS_CMD='exit 0' bash scripts/worktree.sh mr c1 2>&1)" || { fail "commit -> mr" "$out"; return; }
|
|
1954
|
+
out="$(bash scripts/worktree.sh merge c1 2>&1)" || { fail "commit -> mr -> merge" "$out"; return; }
|
|
1955
|
+
if [ -f new-file.txt ] && [ "$(git log --merges --oneline | wc -l)" -ge 1 ]; then
|
|
1956
|
+
pass "commit -> mr -> merge end to end (--no-ff merge commit, file on sprint branch)"
|
|
1957
|
+
else
|
|
1958
|
+
fail "commit end to end" "new-file.txt missing or no merge commit"
|
|
1959
|
+
fi
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
test_commit_forbidden_path_nothing_committed() {
|
|
1963
|
+
setup_commit_repo "c2"
|
|
1964
|
+
trap cleanup_repo RETURN
|
|
1965
|
+
local before rc=0
|
|
1966
|
+
before="$(git -C "$CWT" rev-parse HEAD)"
|
|
1967
|
+
mkdir -p "$CWT/.githooks"; echo "evil" > "$CWT/.githooks/pre-commit"
|
|
1968
|
+
echo "fine" > "$CWT/ok.txt"
|
|
1969
|
+
bash scripts/worktree.sh commit c2 --runner kilo >/dev/null 2>&1 || rc=$?
|
|
1970
|
+
if [ "$rc" -eq 4 ] && [ "$(git -C "$CWT" rev-parse HEAD)" = "$before" ]; then
|
|
1971
|
+
pass "commit: forbidden path -> exit 4, nothing committed"
|
|
1972
|
+
else
|
|
1973
|
+
fail "commit forbidden path" "rc=$rc head moved=$([ "$(git -C "$CWT" rev-parse HEAD)" = "$before" ] && echo no || echo yes)"
|
|
1974
|
+
fi
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
test_commit_generated_file_forbidden() {
|
|
1978
|
+
setup_commit_repo "c3"
|
|
1979
|
+
trap cleanup_repo RETURN
|
|
1980
|
+
local rc=0
|
|
1981
|
+
mkdir -p "$CWT/tasks"; echo "{}" > "$CWT/tasks/tasks.snapshot.json"
|
|
1982
|
+
bash scripts/worktree.sh commit c3 --runner junie >/dev/null 2>&1 || rc=$?
|
|
1983
|
+
if [ "$rc" -eq 4 ]; then
|
|
1984
|
+
pass "commit: generated tracker files are refused (exit 4)"
|
|
1985
|
+
else
|
|
1986
|
+
fail "commit generated file" "expected 4, got $rc"
|
|
1987
|
+
fi
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
test_commit_no_changes_exit1() {
|
|
1991
|
+
setup_commit_repo "c4"
|
|
1992
|
+
trap cleanup_repo RETURN
|
|
1993
|
+
local rc=0
|
|
1994
|
+
bash scripts/worktree.sh commit c4 --runner junie >/dev/null 2>&1 || rc=$?
|
|
1995
|
+
if [ "$rc" -eq 1 ]; then
|
|
1996
|
+
pass "commit: no changes -> exit 1"
|
|
1997
|
+
else
|
|
1998
|
+
fail "commit no changes" "expected 1, got $rc"
|
|
1999
|
+
fi
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
echo ""
|
|
2003
|
+
echo "=== commit command tests ==="
|
|
2004
|
+
|
|
2005
|
+
test_commit_identity_message_and_flow
|
|
2006
|
+
test_commit_forbidden_path_nothing_committed
|
|
2007
|
+
test_commit_generated_file_forbidden
|
|
2008
|
+
test_commit_no_changes_exit1
|
|
2009
|
+
|
|
2010
|
+
test_sprint_start_from_main_and_from_flag() {
|
|
2011
|
+
make_repo
|
|
2012
|
+
trap cleanup_repo RETURN
|
|
2013
|
+
|
|
2014
|
+
echo "initial" > file.txt
|
|
2015
|
+
git add -A && git commit -q -m "initial"
|
|
2016
|
+
git checkout -q -b other && echo x > x.txt && git add -A && git commit -q -m "other work"
|
|
2017
|
+
bash scripts/worktree.sh sprint start 2 base >/dev/null
|
|
2018
|
+
if [ "$(git rev-parse sprint/2-base)" = "$(git rev-parse main)" ]; then
|
|
2019
|
+
pass "sprint start defaults to main, not the checked-out branch"
|
|
2020
|
+
else
|
|
2021
|
+
fail "sprint start base" "sprint/2-base is not at main"
|
|
2022
|
+
fi
|
|
2023
|
+
git checkout -q other
|
|
2024
|
+
bash scripts/worktree.sh sprint start 3 cascade --from other >/dev/null
|
|
2025
|
+
if [ "$(git rev-parse sprint/3-cascade)" = "$(git rev-parse other)" ]; then
|
|
2026
|
+
pass "sprint start --from builds on an explicit branch (cascade)"
|
|
2027
|
+
else
|
|
2028
|
+
fail "sprint start --from" "sprint/3-cascade is not at other"
|
|
2029
|
+
fi
|
|
2030
|
+
local rc=0
|
|
2031
|
+
bash scripts/worktree.sh sprint start 4 bad --from nope >/dev/null 2>&1 || rc=$?
|
|
2032
|
+
[ "$rc" -eq 1 ] && pass "sprint start refuses unknown --from" || fail "sprint start unknown --from" "exit $rc"
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
test_new_refuses_task_of_another_sprint() {
|
|
2036
|
+
make_repo
|
|
2037
|
+
trap cleanup_repo RETURN
|
|
2038
|
+
|
|
2039
|
+
echo "initial" > file.txt
|
|
2040
|
+
git add -A && git commit -q -m "initial"
|
|
2041
|
+
bash scripts/worktree.sh sprint start 7 other >/dev/null
|
|
2042
|
+
local rc=0
|
|
2043
|
+
bash scripts/worktree.sh new wrong --task-id 6.2 >/dev/null 2>&1 || rc=$?
|
|
2044
|
+
if [ "$rc" -eq 11 ] && [ ! -e tmp/worktrees/wrong ] && ! git rev-parse --verify -q task/wrong >/dev/null; then
|
|
2045
|
+
pass "new refuses task 6.2 on sprint/7-* (exit 11, nothing created)"
|
|
2046
|
+
else
|
|
2047
|
+
fail "new wrong sprint" "exit $rc"
|
|
2048
|
+
fi
|
|
2049
|
+
rc=0
|
|
2050
|
+
bash scripts/worktree.sh new right --task-id 7.1 >/dev/null 2>&1 || rc=$?
|
|
2051
|
+
[ "$rc" -eq 0 ] && pass "new accepts task 7.1 on sprint/7-*" || fail "new right sprint" "exit $rc"
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
test_merge_refuses_when_main_tree_on_other_sprint() {
|
|
2055
|
+
make_repo
|
|
2056
|
+
trap cleanup_repo RETURN
|
|
2057
|
+
|
|
2058
|
+
echo "initial" > file.txt
|
|
2059
|
+
git add -A && git commit -q -m "initial"
|
|
2060
|
+
bash scripts/worktree.sh sprint start 6 mine >/dev/null
|
|
2061
|
+
bash scripts/worktree.sh new t6 --task-id 6.1 >/dev/null
|
|
2062
|
+
git checkout -q main
|
|
2063
|
+
bash scripts/worktree.sh sprint start 7 theirs >/dev/null
|
|
2064
|
+
local rc=0
|
|
2065
|
+
bash scripts/worktree.sh merge t6 >/dev/null 2>&1 || rc=$?
|
|
2066
|
+
if [ "$rc" -eq 11 ] && [ "$(git symbolic-ref --short HEAD)" = "sprint/7-theirs" ]; then
|
|
2067
|
+
pass "merge refuses across sprints and does not switch the main tree"
|
|
2068
|
+
else
|
|
2069
|
+
fail "merge across sprints" "exit $rc, branch $(git symbolic-ref --short HEAD)"
|
|
2070
|
+
fi
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
test_new_on_fresh_clone_without_tmp() {
|
|
2074
|
+
make_repo
|
|
2075
|
+
trap cleanup_repo RETURN
|
|
2076
|
+
echo "initial" > file.txt
|
|
2077
|
+
git add -A && git commit -q -m "initial"
|
|
2078
|
+
git branch -m main 2>/dev/null || true
|
|
2079
|
+
bash scripts/worktree.sh sprint start 1 test >/dev/null
|
|
2080
|
+
rm -rf tmp # a fresh clone has no git-ignored tmp/ at all
|
|
2081
|
+
local rc=0
|
|
2082
|
+
bash scripts/worktree.sh new fresh >/dev/null 2>&1 || rc=$?
|
|
2083
|
+
if [ "$rc" -eq 0 ] && [ -f tmp/meta/fresh.base ]; then
|
|
2084
|
+
pass "new works on a fresh clone without tmp/ (creates tmp/meta)"
|
|
2085
|
+
else
|
|
2086
|
+
fail "new on fresh clone" "rc=$rc meta=$([ -f tmp/meta/fresh.base ] && echo yes || echo missing)"
|
|
2087
|
+
fi
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
test_new_on_fresh_clone_without_tmp
|
|
2091
|
+
test_accept_fails_loudly_when_links_cannot_be_recorded
|
|
2092
|
+
test_accept_links_only_retries_the_links
|
|
2093
|
+
test_sprint_run_reports_results_and_planner_failure
|
|
2094
|
+
test_sprint_run_reports_failed_task_without_stale_mrs
|
|
2095
|
+
test_sprint_run_lists_only_mrs_created_by_run
|
|
2096
|
+
test_sprint_start_from_main_and_from_flag
|
|
2097
|
+
test_new_refuses_task_of_another_sprint
|
|
2098
|
+
test_merge_refuses_when_main_tree_on_other_sprint
|
|
2099
|
+
|
|
2100
|
+
echo ""
|
|
2101
|
+
echo "=== apply removed tests ==="
|
|
2102
|
+
|
|
2103
|
+
test_apply_command_removed
|
|
2104
|
+
test_patch_command_still_works
|
|
2105
|
+
|
|
2106
|
+
echo ""
|
|
2107
|
+
echo "=== Results: $passed/$total passed, $failed failed ==="
|
|
2108
|
+
|
|
2109
|
+
if [ "$failed" -gt 0 ]; then
|
|
2110
|
+
exit 1
|
|
2111
|
+
fi
|
|
2112
|
+
exit 0
|