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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
//! Tests for edge cases and bug fixes
//!
//! These tests verify that various edge cases are handled correctly.
use crate::common;
use common::{OutputAssertions, TestRepo};
// =============================================================================
// 1. REPARENT CIRCULAR DEPENDENCY DETECTION
// =============================================================================
/// Reparenting a branch to one of its descendants should fail
/// (prevents circular dependency: A -> B -> C, then reparent A onto C)
#[test]
fn test_reparent_rejects_circular_dependency() {
let repo = TestRepo::new();
// Create a stack: main -> branch-a -> branch-b -> branch-c
repo.create_stack(&["branch-a", "branch-b", "branch-c"]);
// Try to reparent branch-a onto branch-c (its descendant)
// This should fail because it would create: C -> A -> B -> C (circular)
let branch_a = repo.find_branch_containing("branch-a").unwrap();
let branch_c = repo.find_branch_containing("branch-c").unwrap();
let output = repo.run_stax(&["branch", "reparent", "-b", &branch_a, "-p", &branch_c]);
// Should fail with clear error about circular dependency
output.assert_failure();
let stderr = TestRepo::stderr(&output);
assert!(
stderr.contains("circular") || stderr.contains("descendant") || stderr.contains("ancestor"),
"Expected error about circular dependency, got: {}",
stderr
);
}
/// Reparenting a branch onto its direct child should fail
#[test]
fn test_reparent_rejects_direct_child_as_parent() {
let repo = TestRepo::new();
// Create: main -> parent -> child
repo.create_stack(&["parent-branch", "child-branch"]);
let parent = repo.find_branch_containing("parent-branch").unwrap();
let child = repo.find_branch_containing("child-branch").unwrap();
// Try to reparent parent onto child
let output = repo.run_stax(&["branch", "reparent", "-b", &parent, "-p", &child]);
output.assert_failure();
}
// =============================================================================
// 2. FOLD COMMAND RECOVERY ON CONFLICT
// =============================================================================
/// Fold should restore original branch on merge conflict
#[test]
fn test_fold_restores_branch_on_conflict() {
let repo = TestRepo::new();
// Create a branch
repo.run_stax(&["create", "feature"]).assert_success();
let feature_branch = repo.current_branch();
// Create conflicting changes on feature
repo.create_file("conflict.txt", "feature content");
repo.commit("Feature commit");
// Go to main and create conflicting file
repo.run_stax(&["checkout", "main"]).assert_success();
repo.create_file("conflict.txt", "main content");
repo.commit("Main commit");
// Create another branch from main that will conflict with feature
repo.run_stax(&["create", "middle"]).assert_success();
let middle_branch = repo.current_branch();
repo.create_file("middle.txt", "middle content");
repo.commit("Middle commit");
// Reparent feature onto middle
repo.run_stax(&["checkout", &feature_branch])
.assert_success();
repo.run_stax(&[
"branch",
"reparent",
"-b",
&feature_branch,
"-p",
&middle_branch,
])
.assert_success();
// Now try to fold feature into middle - should fail due to conflict
// but should restore us to the feature branch
let output = repo.run_stax(&["branch", "fold", "--yes"]);
// Even if fold fails, we should still be on our original branch
// (not left on the parent branch in a bad state)
let current = repo.current_branch();
assert!(
current.contains("feature") || output.status.success(),
"After failed fold, should be on original branch '{}', but on '{}'",
feature_branch,
current
);
}
/// Fold with --yes flag should work without prompting
#[test]
fn test_fold_yes_flag_no_prompt() {
let repo = TestRepo::new();
// Create: main -> parent -> child
repo.run_stax(&["create", "parent-fold"]).assert_success();
repo.create_file("parent.txt", "parent");
repo.commit("Parent commit");
repo.run_stax(&["create", "child-fold"]).assert_success();
repo.create_file("child.txt", "child");
repo.commit("Child commit");
// Fold child into parent with --yes (no prompt)
let output = repo.run_stax(&["branch", "fold", "--yes"]);
output.assert_success();
// Should now be on parent branch
assert!(repo.current_branch().contains("parent-fold"));
}
// =============================================================================
// 3. MERGE DIRTY WORKING TREE CHECK
// =============================================================================
/// Merge should warn/fail if working tree is dirty
#[test]
fn test_merge_warns_on_dirty_tree() {
let repo = TestRepo::new_with_remote();
// Create a branch with a commit
repo.run_stax(&["create", "dirty-test"]).assert_success();
repo.create_file("feature.txt", "feature");
repo.commit("Feature commit");
// Create uncommitted changes
repo.create_file("uncommitted.txt", "dirty");
// Try to run merge (even though there's nothing to merge, it should check dirty state)
let output = repo.run_stax(&["merge", "--yes"]);
// Should either fail or warn about dirty working tree
let combined = format!("{}{}", TestRepo::stdout(&output), TestRepo::stderr(&output));
// If it ran at all, it should have mentioned dirty/uncommitted/stash
// or it should have failed
if output.status.success() {
// If it succeeded, it should have stashed or warned
assert!(
combined.contains("stash")
|| combined.contains("dirty")
|| combined.contains("uncommitted")
|| combined.contains("Nothing to merge"),
"Merge with dirty tree should warn about uncommitted changes"
);
}
// If it failed, that's also acceptable behavior
}
// =============================================================================
// 4. DETACHED HEAD HANDLING
// =============================================================================
/// Commands should handle detached HEAD gracefully
#[test]
fn test_status_handles_detached_head() {
let repo = TestRepo::new();
// Create a branch and get its commit
repo.run_stax(&["create", "detach-test"]).assert_success();
repo.create_file("test.txt", "test");
repo.commit("Test commit");
let commit_sha = repo.head_sha();
// Detach HEAD
repo.git(&["checkout", "--detach", &commit_sha]);
// Status should not panic, should give helpful message
let output = repo.run_stax(&["status"]);
let combined = format!("{}{}", TestRepo::stdout(&output), TestRepo::stderr(&output));
// Should either succeed with some output or fail with helpful message
assert!(
output.status.success() || combined.contains("detached") || combined.contains("HEAD"),
"Status in detached HEAD should handle gracefully, got: {}",
combined
);
}
/// Navigation commands should handle detached HEAD
#[test]
fn test_navigate_handles_detached_head() {
let repo = TestRepo::new();
// Create branches
repo.create_stack(&["nav-a", "nav-b"]);
let commit_sha = repo.head_sha();
// Detach HEAD
repo.git(&["checkout", "--detach", &commit_sha]);
// Navigation should not panic
let output = repo.run_stax(&["down"]);
let stderr = TestRepo::stderr(&output);
// Should fail gracefully with helpful message
assert!(
output.status.success() || stderr.contains("detached") || stderr.contains("branch"),
"Navigation in detached HEAD should handle gracefully, got: {}",
stderr
);
}
// =============================================================================
// 5. NON-INTERACTIVE FLAGS FOR COMMANDS
// =============================================================================
/// Squash with --yes flag should work without prompting
#[test]
fn test_squash_yes_flag_no_prompt() {
let repo = TestRepo::new();
// Create a branch with multiple commits
repo.run_stax(&["create", "squash-test"]).assert_success();
repo.create_file("file1.txt", "content1");
repo.commit("Commit 1");
repo.create_file("file2.txt", "content2");
repo.commit("Commit 2");
// Squash with --yes and a message
let output = repo.run_stax(&["branch", "squash", "--yes", "-m", "Squashed commit"]);
output.assert_success();
}
/// Undo with --yes flag should work without prompting
#[test]
fn test_undo_yes_flag_no_prompt() {
let repo = TestRepo::new();
// Create a branch (this creates a transaction we could undo)
repo.run_stax(&["create", "undo-test"]).assert_success();
repo.create_file("test.txt", "test");
repo.commit("Test commit");
// Do a restack to create something to undo
repo.run_stax(&["checkout", "main"]).assert_success();
repo.create_file("main.txt", "main update");
repo.commit("Main update");
let undo_branch = repo.find_branch_containing("undo-test").unwrap();
repo.run_stax(&["checkout", &undo_branch]).assert_success();
repo.run_stax(&["restack"]).assert_success();
// Now undo with --yes
let output = repo.run_stax(&["undo", "--yes"]);
// Should either succeed or say nothing to undo (both are valid)
let combined = format!("{}{}", TestRepo::stdout(&output), TestRepo::stderr(&output));
assert!(
output.status.success() || combined.contains("nothing") || combined.contains("No undo"),
"Undo --yes should work without prompting"
);
}
/// Redo with --yes flag should work without prompting
#[test]
fn test_redo_yes_flag_no_prompt() {
let repo = TestRepo::new();
// Just verify the flag exists and doesn't hang
let output = repo.run_stax(&["redo", "--yes"]);
// Should either succeed or say nothing to redo
let combined = format!("{}{}", TestRepo::stdout(&output), TestRepo::stderr(&output));
assert!(
output.status.success()
|| combined.contains("nothing")
|| combined.contains("No redo")
|| combined.contains("No operations"),
"Redo --yes should work without prompting, got: {}",
combined
);
}
// =============================================================================
// 6. STACK INTEGRITY
// =============================================================================
/// Status should detect and warn about circular parent references
#[test]
fn test_status_detects_circular_references() {
let repo = TestRepo::new();
// Create some branches normally
repo.create_stack(&["branch-a", "branch-b"]);
// Manually corrupt metadata to create circular reference
// This is simulating what could happen if reparent didn't check
// We'll skip this test if we can't easily corrupt metadata
// For now, just verify status runs without infinite loop
let output = repo.run_stax(&["status", "--json"]);
// Should complete (not hang in infinite loop)
// This test passing means status handles the stack correctly
assert!(
output.status.success(),
"Status should complete without hanging"
);
}
// =============================================================================
// 7. ERROR RECOVERY
// =============================================================================
/// Stash pop failure should be reported to user
#[test]
fn test_restack_reports_stash_pop_failure() {
let repo = TestRepo::new();
// Create a branch
repo.run_stax(&["create", "stash-test"]).assert_success();
repo.create_file("feature.txt", "feature content");
repo.commit("Feature commit");
// Go to main and update
repo.run_stax(&["checkout", "main"]).assert_success();
repo.create_file("main.txt", "main content");
repo.commit("Main update");
// Go back to feature
let feature = repo.find_branch_containing("stash-test").unwrap();
repo.run_stax(&["checkout", &feature]).assert_success();
// Create dirty state that will conflict on stash pop
repo.create_file("conflict.txt", "dirty state");
// Manually stash
repo.git(&["stash", "push", "-m", "test stash"]);
// Create a file that will conflict with stash
repo.create_file("conflict.txt", "different content");
repo.commit("Conflicting commit");
// Now restack with stash - the stash pop should fail
// The test verifies we handle this case (even if it's just a warning)
let output = repo.run_stax(&["restack", "--yes"]);
// This test documents expected behavior - we should at least not crash
// and ideally warn the user about stash issues
assert!(
output.status.success() || !TestRepo::stderr(&output).is_empty(),
"Restack should handle stash pop issues gracefully"
);
}