use crate::common;
use common::{OutputAssertions, TestRepo};
#[test]
fn test_checkout_to_trunk() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["checkout", "main"]);
output.assert_success();
assert_eq!(repo.current_branch(), "main");
}
#[test]
fn test_checkout_alias_co() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["co", "main"]);
output.assert_success();
assert_eq!(repo.current_branch(), "main");
}
#[test]
fn test_checkout_nonexistent_branch() {
let repo = TestRepo::new();
let output = repo.run_stax(&["checkout", "nonexistent-branch"]);
output.assert_failure();
}
#[test]
fn test_checkout_to_tracked_branch() {
let repo = TestRepo::new();
let branches = repo.create_stack(&["feature-a", "feature-b"]);
let output = repo.run_stax(&["checkout", &branches[0]]);
output.assert_success();
assert!(repo.current_branch_contains("feature-a"));
}
#[test]
fn test_checkout_by_partial_match() {
let repo = TestRepo::new();
repo.create_stack(&["unique-feature"]);
repo.run_stax(&["checkout", "main"]);
let output = repo.run_stax(&["checkout", "unique"]);
if output.status.success() {
assert!(repo.current_branch_contains("unique"));
}
}
#[test]
fn test_checkout_trunk_alias_t() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["t"]);
output.assert_success();
assert_eq!(repo.current_branch(), "main");
}
#[test]
fn test_checkout_trunk_full_command() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["trunk"]);
output.assert_success();
assert_eq!(repo.current_branch(), "main");
}
#[test]
fn test_trunk_set_existing_branch() {
let repo = TestRepo::new();
repo.git(&["branch", "develop"]);
let output = repo.run_stax(&["trunk", "develop"]);
output
.assert_success()
.assert_stdout_contains("Trunk branch set to 'develop'");
}
#[test]
fn test_trunk_set_nonexistent_branch_fails() {
let repo = TestRepo::new();
let output = repo.run_stax(&["trunk", "does-not-exist"]);
output
.assert_failure()
.assert_stderr_contains("does not exist locally");
}
#[test]
fn test_get_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["get", "--help"]);
output
.assert_success()
.assert_stdout_contains("remote branch")
.assert_stdout_contains("--parent")
.assert_stdout_contains("--no-checkout")
.assert_stdout_contains("--force");
}
#[test]
fn test_full_command_reference_mentions_visible_top_level_commands() {
let repo = TestRepo::new();
let output = repo.run_stax(&["--help"]);
output.assert_success();
let stdout = TestRepo::stdout(&output);
let docs = include_str!("../docs/commands/reference.md");
let mut in_commands = false;
let mut missing = Vec::new();
for line in stdout.lines() {
if line.starts_with("Commands:") {
in_commands = true;
continue;
}
if line.starts_with("Options:") {
in_commands = false;
}
if !in_commands || line.trim().is_empty() {
continue;
}
let Some(command) = line.split_whitespace().next() else {
continue;
};
if command == "help" {
continue;
}
let documented = docs.contains(&format!("`st {command}"))
|| docs.contains(&format!("`stax {command}"))
|| docs.contains(&format!("### `{command}`"));
if !documented {
missing.push(command.to_string());
}
}
assert!(
missing.is_empty(),
"docs/commands/reference.md is missing visible top-level commands: {}",
missing.join(", ")
);
}
#[test]
fn docs_clarify_stack_unlink_requires_gh_stack_local_tracking() {
let docs = [
("README.md", include_str!("../README.md")),
(
"docs/commands/core.md",
include_str!("../docs/commands/core.md"),
),
(
"docs/commands/reference.md",
include_str!("../docs/commands/reference.md"),
),
("skills.md", include_str!("../skills.md")),
];
for (path, content) in docs {
assert!(
!content.contains("Remove native GitHub Stack object")
&& !content.contains("remove the native GitHub Stack object")
&& !content.contains("Remove the native GitHub Stack object"),
"{path} should not imply `st stack unlink` directly removes every native GitHub Stack object"
);
assert!(
content.contains("gh stack checkout <pr>") || content.contains("locally tracked"),
"{path} should mention gh-stack local tracking or `gh stack checkout <pr>` for `st stack unlink`"
);
}
}
#[test]
fn test_sync_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["sync", "--help"]);
output.assert_success();
output.assert_stdout_contains("Sync");
}
#[test]
fn test_sync_alias_rs_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["rs", "--help"]);
output.assert_success();
}
#[test]
fn test_init_command_sets_trunk_branch() {
let repo = TestRepo::new();
let git_output = repo.git(&["branch", "master"]);
assert!(
git_output.status.success(),
"{}",
TestRepo::stderr(&git_output)
);
let output = repo.run_stax(&["init", "--trunk", "master"]);
output.assert_success();
let json = repo.get_status_json();
assert_eq!(json["trunk"], "master");
let trunk_output = repo.run_stax(&["trunk"]);
trunk_output.assert_success();
assert_eq!(repo.current_branch(), "master");
}
#[test]
fn test_restack_no_changes_needed() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["restack"]);
output.assert_success();
}
#[test]
fn test_restack_with_changes() {
let repo = TestRepo::new();
let branches = repo.create_stack(&["feature-a", "feature-b"]);
repo.run_stax(&["checkout", &branches[0]]);
repo.create_file("extra.txt", "extra content");
repo.commit("Extra commit");
repo.run_stax(&["checkout", &branches[1]]);
let output = repo.run_stax(&["restack"]);
output.assert_success();
}
#[test]
fn test_restack_quiet_flag() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["restack", "--quiet"]);
output.assert_success();
}
#[test]
fn test_restack_stop_here_flag() {
let repo = TestRepo::new();
repo.create_stack(&["feature-a", "feature-b"]);
let output = repo.run_stax(&["restack", "--stop-here", "--quiet"]);
output.assert_success();
}
#[test]
fn test_restack_submit_after_no_flag() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["restack", "--submit-after", "no"]);
output.assert_success();
}
#[test]
fn test_restack_on_trunk() {
let repo = TestRepo::new();
let output = repo.run_stax(&["restack"]);
output.assert_success();
}
#[test]
fn test_restack_continue_no_rebase() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["restack", "--continue"]);
let _ = output;
}
#[test]
fn test_upstack_restack() {
let repo = TestRepo::new();
let branches = repo.create_stack(&["feature-a", "feature-b", "feature-c"]);
repo.run_stax(&["checkout", &branches[0]]);
let output = repo.run_stax(&["upstack", "restack"]);
output.assert_success();
}
#[test]
fn test_undo_no_operations() {
let repo = TestRepo::new();
let output = repo.run_stax(&["undo"]);
output.assert_failure();
}
#[test]
fn test_undo_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["undo", "--help"]);
output.assert_success();
output.assert_stdout_contains("Undo");
}
#[test]
fn test_redo_no_operations() {
let repo = TestRepo::new();
let output = repo.run_stax(&["redo"]);
output.assert_failure();
}
#[test]
fn test_redo_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["redo", "--help"]);
output.assert_success();
output.assert_stdout_contains("Redo");
}
#[test]
fn test_doctor_basic() {
let repo = TestRepo::new();
let output = repo.run_stax(&["doctor"]);
output.assert_success();
}
#[test]
fn test_doctor_with_stack() {
let repo = TestRepo::new();
repo.create_stack(&["feature-a", "feature-b"]);
let output = repo.run_stax(&["doctor"]);
output.assert_success();
}
#[test]
fn test_doctor_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["doctor", "--help"]);
output.assert_success();
}
#[test]
fn test_log_on_trunk() {
let repo = TestRepo::new();
let output = repo.run_stax(&["log"]);
output.assert_success();
}
#[test]
fn test_log_on_branch() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["log"]);
output.assert_success();
}
#[test]
fn test_log_with_stack() {
let repo = TestRepo::new();
repo.create_stack(&["feature-a", "feature-b", "feature-c"]);
let output = repo.run_stax(&["log"]);
output.assert_success();
}
#[test]
fn test_log_alias_l() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["l"]);
output.assert_success();
}
#[test]
fn test_log_alias_ll() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["ll"]);
output.assert_success();
}
#[test]
fn test_log_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["log", "--help"]);
output.assert_success();
}
#[test]
fn test_diff_on_branch() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["diff"]);
output.assert_success();
}
#[test]
fn test_diff_with_changes() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
repo.create_file("new-file.txt", "content");
repo.commit("Add new file");
let output = repo.run_stax(&["diff"]);
output.assert_success();
}
#[test]
fn test_diff_alias_d() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["d"]);
output.assert_success();
}
#[test]
fn test_diff_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["diff", "--help"]);
output.assert_success();
}
#[test]
fn test_range_diff_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["range-diff", "--help"]);
output.assert_success();
}
#[test]
fn test_modify_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["modify", "--help"]);
output.assert_success();
output.assert_stdout_contains("modify");
}
#[test]
fn test_modify_alias_m() {
let repo = TestRepo::new();
let output = repo.run_stax(&["m", "--help"]);
output.assert_success();
}
#[test]
fn test_pr_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["pr", "--help"]);
output.assert_success();
output.assert_stdout_contains("open");
output.assert_stdout_contains("list");
}
#[test]
fn test_pr_open_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["pr", "open", "--help"]);
output.assert_success();
}
#[test]
fn test_pr_list_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["pr", "list", "--help"]);
output.assert_success();
output.assert_stdout_contains("limit");
output.assert_stdout_contains("json");
}
#[test]
fn test_issue_list_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["issue", "list", "--help"]);
output.assert_success();
output.assert_stdout_contains("limit");
output.assert_stdout_contains("json");
}
#[test]
fn test_pr_without_remote() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["pr"]);
output.assert_failure();
}
#[test]
fn test_submit_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["submit", "--help"]);
output.assert_success();
output.assert_stdout_contains("Submit");
output.assert_stdout_contains("open");
output.assert_stdout_contains("verbose");
}
#[test]
fn test_submit_alias_ss() {
let repo = TestRepo::new();
let output = repo.run_stax(&["ss", "--help"]);
output.assert_success();
}
#[test]
fn test_submit_without_remote() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["submit"]);
output.assert_failure();
}
#[test]
fn test_submit_draft_flag() {
let repo = TestRepo::new_with_remote();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["submit", "--help"]);
output.assert_success();
output.assert_stdout_contains("draft");
}
#[test]
fn test_branch_create_with_message() {
let repo = TestRepo::new();
let output = repo.run_stax(&["bc", "test-branch", "-m", "Test message"]);
output.assert_success();
assert!(repo.current_branch_contains("test-branch"));
}
#[test]
fn test_branch_create_from_trunk() {
let repo = TestRepo::new();
let output = repo.run_stax(&["bc", "from-trunk"]);
output.assert_success();
}
#[test]
fn test_branch_create_stacked() {
let repo = TestRepo::new();
repo.create_stack(&["parent"]);
let output = repo.run_stax(&["bc", "child"]);
output.assert_success();
let parent = repo.get_current_parent();
assert!(parent.is_some());
assert!(parent.unwrap().contains("parent"));
}
#[test]
fn test_branch_delete_untracked() {
let repo = TestRepo::new();
repo.git(&["checkout", "-b", "untracked-branch"]);
repo.git(&["checkout", "main"]);
let output = repo.run_stax(&["bd", "untracked-branch", "--force"]);
let _ = output;
}
#[test]
fn test_branch_delete_current_fails() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let current = repo.current_branch();
let output = repo.run_stax(&["bd", ¤t]);
output.assert_failure();
}
#[test]
fn test_branch_delete_with_children() {
let repo = TestRepo::new();
let branches = repo.create_stack(&["parent", "child"]);
repo.run_stax(&["t"]);
let output = repo.run_stax(&["bd", &branches[0]]);
output.assert_failure();
}
#[test]
fn test_branch_rename() {
let repo = TestRepo::new();
repo.create_stack(&["old-name"]);
let output = repo.run_stax(&["branch", "rename", "new-name"]);
output.assert_success();
assert!(repo.current_branch_contains("new-name"));
}
#[test]
fn test_branch_rename_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["branch", "rename", "--help"]);
output.assert_success();
}
#[test]
fn test_branch_track_untracked() {
let repo = TestRepo::new();
repo.git(&["checkout", "-b", "untracked"]);
repo.create_file("untracked.txt", "content");
repo.commit("Untracked commit");
let output = repo.run_stax(&["branch", "track", "--parent", "main"]);
output.assert_success();
}
#[test]
fn test_branch_track_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["branch", "track", "--help"]);
output.assert_success();
}
#[test]
fn test_branch_untrack_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["branch", "untrack", "--help"]);
output.assert_success();
}
#[test]
fn test_branch_squash_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["branch", "squash", "--help"]);
output.assert_success();
}
#[test]
fn test_branch_squash_single_commit() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["branch", "squash"]);
let _ = output;
}
#[test]
fn test_auth_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["auth", "--help"]);
output.assert_success();
output.assert_stdout_contains("auth");
}
#[test]
fn test_auth_status() {
let repo = TestRepo::new();
let output = repo.run_stax(&["auth", "status"]);
let _ = output;
}
#[test]
fn test_config_show() {
let repo = TestRepo::new();
let output = repo.run_stax(&["config"]);
output.assert_success();
}
#[test]
fn test_config_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["config", "--help"]);
output.assert_success();
}
#[test]
fn test_status_shows_branch_info() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["status"]);
output.assert_success();
output.assert_stdout_contains("feature");
}
#[test]
fn test_bare_command_falls_back_to_status_noninteractive() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&[]);
output.assert_success();
output.assert_stdout_contains("feature");
}
#[test]
fn test_merge_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["merge", "--help"]);
output.assert_success();
}
#[test]
fn test_children_relationship() {
let repo = TestRepo::new();
let branches = repo.create_stack(&["parent", "child"]);
let children = repo.get_children(&branches[0]);
assert_eq!(children.len(), 1);
assert!(children[0].contains("child"));
}
#[test]
fn test_downstack_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["downstack", "--help"]);
output.assert_success();
}
#[test]
fn test_upstack_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["upstack", "--help"]);
output.assert_success();
}
#[test]
fn test_branch_reparent_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["branch", "reparent", "--help"]);
output.assert_success();
let stdout = TestRepo::stdout(&output);
assert!(
stdout.contains("--restack"),
"help should document --restack, got: {}",
stdout
);
}
#[test]
fn test_create_alias_c() {
let repo = TestRepo::new();
let output = repo.run_stax(&["c", "new-feature"]);
output.assert_success();
assert!(repo.current_branch_contains("new-feature"));
}
#[test]
fn test_create_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["create", "--help"]);
output.assert_success();
}
#[test]
fn test_rename_command() {
let repo = TestRepo::new();
repo.create_stack(&["old-name"]);
let output = repo.run_stax(&["rename", "new-name-2"]);
output.assert_success();
assert!(repo.current_branch_contains("new-name-2"));
}
#[test]
fn test_rename_help() {
let repo = TestRepo::new();
let output = repo.run_stax(&["rename", "--help"]);
output.assert_success();
}
#[test]
fn test_status_json_basic() {
let repo = TestRepo::new();
repo.create_stack(&["feature"]);
let output = repo.run_stax(&["status", "--json"]);
output.assert_success();
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Invalid JSON");
assert!(json["trunk"].is_string());
assert!(json["branches"].is_array());
}