#![allow(deprecated)]
mod common;
use assert_cmd::Command;
use common::{MockPlatformService, TempJjRepo, github_config, make_pr};
use jj_ryu::graph::build_change_graph;
use jj_ryu::submit::{ExecutionStep, analyze_submission, create_submission_plan};
use predicates::prelude::*;
#[test]
fn test_cli_help() {
let mut cmd = Command::cargo_bin("ryu").unwrap();
cmd.arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Stacked PRs for Jujutsu"));
}
#[test]
fn test_cli_version() {
let mut cmd = Command::cargo_bin("ryu").unwrap();
cmd.arg("--version");
cmd.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn test_submit_help() {
let mut cmd = Command::cargo_bin("ryu").unwrap();
cmd.args(["submit", "--help"]);
cmd.assert()
.success()
.stdout(predicate::str::contains("Submit current stack"));
}
#[test]
fn test_sync_help() {
let mut cmd = Command::cargo_bin("ryu").unwrap();
cmd.args(["sync", "--help"]);
cmd.assert()
.success()
.stdout(predicate::str::contains("Sync current stack"));
}
#[test]
fn test_auth_help() {
let mut cmd = Command::cargo_bin("ryu").unwrap();
cmd.args(["auth", "--help"]);
cmd.assert()
.success()
.stdout(predicate::str::contains("github"))
.stdout(predicate::str::contains("gitlab"));
}
#[test]
fn test_invalid_path() {
let mut cmd = Command::cargo_bin("ryu").unwrap();
cmd.args(["--path", "/nonexistent/path/to/repo"]);
cmd.assert().failure();
}
#[test]
fn test_temp_repo_graph_building() {
let repo = TempJjRepo::new();
repo.build_stack(&[("feat-a", "Add A"), ("feat-b", "Add B")]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
assert!(graph.bookmarks.contains_key("feat-a"));
assert!(graph.bookmarks.contains_key("feat-b"));
let stack = graph.stack.as_ref().expect("test expects stack");
assert_eq!(stack.segments.len(), 2);
}
#[test]
fn test_analyze_real_repo_stack() {
let repo = TempJjRepo::new();
repo.build_stack(&[
("feat-a", "Add A"),
("feat-b", "Add B"),
("feat-c", "Add C"),
]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
let analysis = analyze_submission(&graph, Some("feat-b")).expect("analyze");
assert_eq!(analysis.target_bookmark, "feat-b");
assert_eq!(analysis.segments.len(), 2);
assert_eq!(analysis.segments[0].bookmark.name, "feat-a");
assert_eq!(analysis.segments[1].bookmark.name, "feat-b");
}
#[tokio::test]
async fn test_full_submit_flow_new_stack() {
let repo = TempJjRepo::new();
repo.build_stack(&[("feat-a", "Add feature A"), ("feat-b", "Add feature B")]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
let analysis = analyze_submission(&graph, Some("feat-b")).expect("analyze");
let mock = MockPlatformService::with_config(github_config());
let plan = create_submission_plan(&analysis, &mock, "origin", "main")
.await
.expect("create plan");
assert_eq!(plan.count_creates(), 2);
assert_eq!(plan.count_pushes(), 2);
assert_eq!(plan.count_updates(), 0);
let creates: Vec<_> = plan
.execution_steps
.iter()
.filter_map(|s| match s {
ExecutionStep::CreatePr(c) => Some(c),
_ => None,
})
.collect();
assert_eq!(creates[0].base_branch, "main");
assert_eq!(creates[1].base_branch, "feat-a");
assert!(!creates[0].title.is_empty());
assert!(!creates[1].title.is_empty());
}
#[tokio::test]
async fn test_submit_flow_partial_existing_prs() {
let repo = TempJjRepo::new();
repo.build_stack(&[("feat-a", "Add A"), ("feat-b", "Add B")]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
let analysis = analyze_submission(&graph, Some("feat-b")).expect("analyze");
let mock = MockPlatformService::with_config(github_config());
mock.set_find_pr_response("feat-a", Some(make_pr(1, "feat-a", "main")));
let plan = create_submission_plan(&analysis, &mock, "origin", "main")
.await
.expect("create plan");
assert_eq!(plan.count_creates(), 1);
let create = plan
.execution_steps
.iter()
.find_map(|s| match s {
ExecutionStep::CreatePr(c) => Some(c),
_ => None,
})
.expect("should have create step");
assert_eq!(create.bookmark.name, "feat-b");
assert_eq!(plan.existing_prs.len(), 1);
assert!(plan.existing_prs.contains_key("feat-a"));
}
#[tokio::test]
async fn test_submit_flow_base_update_needed() {
let repo = TempJjRepo::new();
repo.build_stack(&[("feat-a", "Add A"), ("feat-b", "Add B")]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
let analysis = analyze_submission(&graph, Some("feat-b")).expect("analyze");
let mock = MockPlatformService::with_config(github_config());
mock.set_find_pr_response("feat-a", Some(make_pr(1, "feat-a", "main")));
mock.set_find_pr_response("feat-b", Some(make_pr(2, "feat-b", "main")));
let plan = create_submission_plan(&analysis, &mock, "origin", "main")
.await
.expect("create plan");
assert_eq!(plan.count_creates(), 0);
assert_eq!(plan.count_updates(), 1);
let update = plan
.execution_steps
.iter()
.find_map(|s| match s {
ExecutionStep::UpdateBase(u) => Some(u),
_ => None,
})
.expect("should have update step");
assert_eq!(update.bookmark.name, "feat-b");
assert_eq!(update.current_base, "main");
assert_eq!(update.expected_base, "feat-a");
}
#[test]
fn test_single_bookmark_stack() {
let repo = TempJjRepo::new();
repo.build_stack(&[("feat-a", "Add feature A")]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
let analysis = analyze_submission(&graph, Some("feat-a")).expect("analyze");
assert_eq!(analysis.segments.len(), 1);
assert_eq!(analysis.segments[0].bookmark.name, "feat-a");
}
#[test]
fn test_empty_repo_no_bookmarks() {
let repo = TempJjRepo::new();
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
assert!(graph.stack.is_none());
assert!(graph.bookmarks.is_empty());
}
#[test]
fn test_three_level_deep_stack() {
let repo = TempJjRepo::new();
repo.build_stack(&[
("feat-a", "Add feature A"),
("feat-b", "Add feature B"),
("feat-c", "Add feature C"),
]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
let stack = graph.stack.as_ref().expect("test expects stack");
assert_eq!(stack.segments.len(), 3);
assert_eq!(stack.segments[0].bookmarks[0].name, "feat-a");
assert_eq!(stack.segments[1].bookmarks[0].name, "feat-b");
assert_eq!(stack.segments[2].bookmarks[0].name, "feat-c");
}
#[tokio::test]
async fn test_plan_verifies_pr_queries_for_stack() {
let repo = TempJjRepo::new();
repo.build_stack(&[
("feat-a", "Add A"),
("feat-b", "Add B"),
("feat-c", "Add C"),
]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
let analysis = analyze_submission(&graph, Some("feat-c")).expect("analyze");
let mock = MockPlatformService::with_config(github_config());
let _ = create_submission_plan(&analysis, &mock, "origin", "main")
.await
.expect("create plan");
mock.assert_find_pr_called_for(&["feat-a", "feat-b", "feat-c"]);
}
#[tokio::test]
async fn test_plan_pr_numbers_increment() {
let repo = TempJjRepo::new();
repo.build_stack(&[("feat-a", "Add A"), ("feat-b", "Add B")]);
let workspace = repo.workspace();
let graph = build_change_graph(&workspace).expect("build graph");
let analysis = analyze_submission(&graph, Some("feat-b")).expect("analyze");
let mock = MockPlatformService::with_config(github_config());
let plan = create_submission_plan(&analysis, &mock, "origin", "main")
.await
.expect("create plan");
assert_eq!(plan.count_creates(), 2);
let creates: Vec<_> = plan
.execution_steps
.iter()
.filter_map(|s| match s {
ExecutionStep::CreatePr(c) => Some(c),
_ => None,
})
.collect();
assert_eq!(creates[0].bookmark.name, "feat-a");
assert_eq!(creates[1].bookmark.name, "feat-b");
}
#[test]
fn test_git_fetch_handles_rebased_commits() {
let (_remote_dir, remote_path) = TempJjRepo::create_bare_remote();
let repo = TempJjRepo::new();
repo.add_remote("origin", &remote_path);
repo.commit("Add feature A");
StdCommand::new("jj")
.args(["bookmark", "create", "feat-a", "-r", "@-"])
.current_dir(repo.path())
.output()
.expect("create bookmark");
repo.push_bookmark("feat-a", "origin");
let temp_clone = TempDir::new().expect("create temp clone dir");
let clone_output = StdCommand::new("git")
.args(["clone", &remote_path.to_string_lossy(), "."])
.current_dir(temp_clone.path())
.output()
.expect("git clone failed");
assert!(
clone_output.status.success(),
"git clone failed: {}",
String::from_utf8_lossy(&clone_output.stderr)
);
let checkout_output = StdCommand::new("git")
.args(["checkout", "feat-a"])
.current_dir(temp_clone.path())
.output()
.expect("git checkout failed");
assert!(
checkout_output.status.success(),
"git checkout failed: {}",
String::from_utf8_lossy(&checkout_output.stderr)
);
StdCommand::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(temp_clone.path())
.output()
.expect("git config email");
StdCommand::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(temp_clone.path())
.output()
.expect("git config name");
let amend_output = StdCommand::new("git")
.args([
"commit",
"--amend",
"--allow-empty",
"-m",
"Add feature A (rebased)",
"--date",
"2026-01-01T00:00:00",
])
.current_dir(temp_clone.path())
.output()
.expect("git amend failed");
assert!(
amend_output.status.success(),
"git commit --amend failed: {}",
String::from_utf8_lossy(&amend_output.stderr)
);
let push_output = StdCommand::new("git")
.args(["push", "--force", "origin", "feat-a"])
.current_dir(temp_clone.path())
.output()
.expect("git push failed");
assert!(
push_output.status.success(),
"git push --force failed: {}",
String::from_utf8_lossy(&push_output.stderr)
);
let mut workspace = repo.workspace();
let result = workspace.git_fetch("origin");
assert!(
result.is_ok(),
"git_fetch should succeed after remote rebase, got: {:?}",
result.err()
);
}
use std::process::Command as StdCommand;
use tempfile::TempDir;