mod test_helpers;
use std::process::Command;
use test_helpers::{jj_available, jjj_binary, run_jjj, setup_test_repo};
#[test]
fn test_init_creates_orphan_from_root() {
if !jj_available() {
return;
}
let dir = tempfile::TempDir::new().expect("Failed to create temp dir");
Command::new("jj")
.args(["git", "init"])
.current_dir(dir.path())
.output()
.expect("jj git init");
Command::new("jj")
.args(["config", "set", "--repo", "user.name", "Test User"])
.current_dir(dir.path())
.status()
.ok();
Command::new("jj")
.args(["config", "set", "--repo", "user.email", "test@example.com"])
.current_dir(dir.path())
.status()
.ok();
std::fs::write(dir.path().join("hello.txt"), "hello").unwrap();
Command::new("jj")
.args(["describe", "-m", "user commit"])
.current_dir(dir.path())
.output()
.expect("jj describe");
let output = Command::new(jjj_binary())
.arg("init")
.current_dir(dir.path())
.output()
.expect("jjj init");
assert!(
output.status.success(),
"jjj init failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let log_output = Command::new("jj")
.args([
"log",
"--no-graph",
"-r",
"ancestors(jjj)",
"-T",
r#"description ++ "\n""#,
])
.current_dir(dir.path())
.output()
.expect("jj log");
let log_stdout = String::from_utf8_lossy(&log_output.stdout);
assert!(
!log_stdout.contains("user commit"),
"jjj bookmark should NOT descend from @; got ancestors:\n{}",
log_stdout
);
}
#[test]
fn test_init_and_create_problem_solution() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir_path = temp_dir.path();
let output = run_jjj(dir_path, &["problem", "new", "Integration Problem"]);
assert!(
output.status.success(),
"problem new failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Integration Problem"));
let output = run_jjj(
dir_path,
&[
"solution",
"new",
"Test Solution",
"--problem",
"Integration Problem",
],
);
assert!(
output.status.success(),
"solution new failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = run_jjj(dir_path, &["solution", "list"]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Test Solution"));
let output = run_jjj(dir_path, &["solution", "show", "Test Solution"]);
assert!(output.status.success(), "solution show failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Integration Problem") || stdout.contains("Addresses:"));
}
#[test]
fn test_critique_workflow() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir_path = temp_dir.path();
run_jjj(dir_path, &["problem", "new", "Test Problem"]);
run_jjj(
dir_path,
&[
"solution",
"new",
"Test Solution",
"--problem",
"Test Problem",
],
);
let output = run_jjj(
dir_path,
&[
"critique",
"new",
"Test Solution",
"This has a flaw",
"--severity",
"high",
],
);
assert!(
output.status.success(),
"critique new failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = run_jjj(dir_path, &["critique", "list"]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("flaw") || stdout.contains("This has a flaw"));
}
#[test]
fn test_problem_hierarchy() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir_path = temp_dir.path();
let output = run_jjj(dir_path, &["problem", "new", "Parent Problem"]);
assert!(output.status.success());
let output = run_jjj(
dir_path,
&[
"problem",
"new",
"Child Problem",
"--parent",
"Parent Problem",
],
);
assert!(
output.status.success(),
"child problem failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = run_jjj(dir_path, &["problem", "show", "Parent Problem"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Child Problem")
|| stdout.contains("Sub-problems")
|| stdout.contains("Child")
);
}
#[test]
fn test_problem_priority() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir_path = temp_dir.path();
let output = run_jjj(
dir_path,
&["problem", "new", "Critical bug", "--priority", "critical"],
);
assert!(
output.status.success(),
"problem new with priority failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = run_jjj(dir_path, &["problem", "show", "Critical bug"]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("critical"),
"Priority not shown in output: {}",
stdout
);
let output = run_jjj(dir_path, &["problem", "new", "Normal bug"]);
assert!(output.status.success());
let output = run_jjj(dir_path, &["problem", "show", "Normal bug"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("medium"),
"Default priority not shown: {}",
stdout
);
}
#[test]
fn test_problem_dissolve_reason() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir_path = temp_dir.path();
let output = run_jjj(dir_path, &["problem", "new", "Ghost bug"]);
assert!(output.status.success());
let output = run_jjj(
dir_path,
&[
"problem",
"dissolve",
"Ghost bug",
"--reason",
"Test data was stale",
],
);
assert!(
output.status.success(),
"dissolve with reason failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = run_jjj(dir_path, &["problem", "show", "Ghost bug"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("dissolved"),
"Status not dissolved: {}",
stdout
);
assert!(
stdout.contains("Test data was stale"),
"Dissolved reason not shown: {}",
stdout
);
}
#[test]
fn test_solution_supersedes() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir_path = temp_dir.path();
run_jjj(dir_path, &["problem", "new", "Slow queries"]);
run_jjj(
dir_path,
&["solution", "new", "Add index", "--problem", "Slow queries"],
);
run_jjj(dir_path, &["solution", "withdraw", "Add index"]);
let output = run_jjj(
dir_path,
&[
"solution",
"new",
"Use connection pool",
"--problem",
"Slow queries",
"--supersedes",
"Add index",
],
);
assert!(
output.status.success(),
"solution new with supersedes failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Supersedes") || stdout.contains("Add index"),
"Supersedes not shown in creation output: {}",
stdout
);
let output = run_jjj(dir_path, &["solution", "show", "Use connection pool"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Supersedes") || stdout.contains("Add index"),
"Supersedes not shown in show: {}",
stdout
);
}
#[test]
fn test_solve_warns_active_solutions() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir_path = temp_dir.path();
run_jjj(dir_path, &["problem", "new", "Fix auth"]);
run_jjj(
dir_path,
&["solution", "new", "Approach A", "--problem", "Fix auth"],
);
run_jjj(dir_path, &["solution", "test", "Approach A"]);
let output = run_jjj(dir_path, &["problem", "solve", "Fix auth"]);
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.success() {
assert!(
stderr.contains("active") || stderr.contains("Warning") || stderr.contains("review"),
"Expected warning about active solutions: stderr={}",
stderr
);
}
}
#[test]
fn test_next_priority_sorting() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir_path = temp_dir.path();
run_jjj(
dir_path,
&["problem", "new", "Low priority task", "--priority", "low"],
);
run_jjj(
dir_path,
&["problem", "new", "Critical issue", "--priority", "critical"],
);
run_jjj(
dir_path,
&["problem", "new", "High priority work", "--priority", "high"],
);
let output = run_jjj(dir_path, &["status", "--json", "--all"]);
assert!(
output.status.success(),
"status failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
let items = json["items"].as_array().expect("items not array");
assert!(
items.len() >= 3,
"Expected at least 3 items, got {}",
items.len()
);
let todo_items: Vec<_> = items
.iter()
.filter(|i| i["category"].as_str() == Some("todo"))
.collect();
assert!(todo_items.len() >= 3, "Expected at least 3 TODO items");
let first_title = todo_items[0]["title"].as_str().unwrap_or("");
assert!(
first_title.contains("Critical"),
"Expected Critical first, got {}",
first_title
);
let second_title = todo_items[1]["title"].as_str().unwrap_or("");
assert!(
second_title.contains("High"),
"Expected High second, got {}",
second_title
);
let third_title = todo_items[2]["title"].as_str().unwrap_or("");
assert!(
third_title.contains("Low"),
"Expected Low third, got {}",
third_title
);
}
#[test]
fn test_critique_new_with_reviewer() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir = temp_dir.path();
run_jjj(dir, &["problem", "new", "Test problem"]);
run_jjj(
dir,
&[
"solution",
"new",
"Test solution",
"--problem",
"Test problem",
],
);
let output = run_jjj(
dir,
&[
"critique",
"new",
"Test solution",
"Review needed",
"--reviewer",
"bob",
],
);
assert!(
output.status.success(),
"critique new with reviewer failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Review needed"),
"Expected title in output: {}",
stdout
);
let show_output = run_jjj(dir, &["critique", "show", "Review needed", "--json"]);
assert!(
show_output.status.success(),
"critique show --json failed: {}",
String::from_utf8_lossy(&show_output.stderr)
);
let show_stdout = String::from_utf8_lossy(&show_output.stdout);
assert!(
show_stdout.contains("\"reviewer\": \"bob\""),
"Expected reviewer: bob in output: {}",
show_stdout
);
}
#[test]
fn test_critique_list_filter_by_reviewer() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir = temp_dir.path();
run_jjj(dir, &["problem", "new", "Test problem"]);
run_jjj(
dir,
&[
"solution",
"new",
"Test solution",
"--problem",
"Test problem",
],
);
run_jjj(
dir,
&[
"critique",
"new",
"Test solution",
"For alice",
"--reviewer",
"alice",
],
);
run_jjj(
dir,
&[
"critique",
"new",
"Test solution",
"For bob",
"--reviewer",
"bob",
],
);
run_jjj(dir, &["critique", "new", "Test solution", "No reviewer"]);
let output = run_jjj(dir, &["critique", "list", "--reviewer", "alice"]);
assert!(
output.status.success(),
"critique list --reviewer failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("For alice"),
"Expected 'For alice' in output: {}",
stdout
);
assert!(
!stdout.contains("For bob"),
"Should not contain 'For bob' in output: {}",
stdout
);
assert!(
!stdout.contains("No reviewer"),
"Should not contain 'No reviewer' in output: {}",
stdout
);
}
#[test]
fn test_solution_new_with_reviewer() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir = temp_dir.path();
run_jjj(dir, &["problem", "new", "Test problem"]);
let output = run_jjj(
dir,
&[
"solution",
"new",
"Test solution",
"--problem",
"Test problem",
"--reviewer",
"bob",
],
);
assert!(
output.status.success(),
"solution new with --reviewer failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let critiques = run_jjj(dir, &["critique", "list", "--json"]);
assert!(
critiques.status.success(),
"critique list --json failed: {}",
String::from_utf8_lossy(&critiques.stderr)
);
let stdout = String::from_utf8_lossy(&critiques.stdout);
assert!(
stdout.contains("Awaiting review from @bob"),
"Expected 'Awaiting review from @bob' in output: {}",
stdout
);
assert!(
stdout.contains("\"reviewer\": \"bob\""),
"Expected '\"reviewer\": \"bob\"' in output: {}",
stdout
);
}
#[test]
fn test_solution_new_with_multiple_reviewers() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir = temp_dir.path();
run_jjj(dir, &["problem", "new", "Test problem"]);
let output = run_jjj(
dir,
&[
"solution",
"new",
"Test solution",
"--problem",
"Test problem",
"--reviewer",
"@alice",
"--reviewer",
"bob:high",
],
);
assert!(
output.status.success(),
"solution new with multiple --reviewer failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let critiques = run_jjj(dir, &["critique", "list", "--json"]);
assert!(
critiques.status.success(),
"critique list --json failed: {}",
String::from_utf8_lossy(&critiques.stderr)
);
let stdout = String::from_utf8_lossy(&critiques.stdout);
assert!(
stdout.contains("Awaiting review from @alice"),
"Expected 'Awaiting review from @alice' in output: {}",
stdout
);
assert!(
stdout.contains("Awaiting review from @bob"),
"Expected 'Awaiting review from @bob' in output: {}",
stdout
);
assert!(
stdout.contains("\"reviewer\": \"alice\""),
"Expected '\"reviewer\": \"alice\"' in output: {}",
stdout
);
assert!(
stdout.contains("\"reviewer\": \"bob\""),
"Expected '\"reviewer\": \"bob\"' in output: {}",
stdout
);
assert!(
stdout.contains("\"severity\": \"high\""),
"Expected '\"severity\": \"high\"' for bob in output: {}",
stdout
);
}
#[test]
fn test_status_shows_review_needed() {
if !jj_available() {
return;
}
let temp_dir = setup_test_repo();
let dir = temp_dir.path();
Command::new("git")
.args(["config", "user.name", "bob"])
.current_dir(dir)
.output()
.expect("Failed to set git user");
run_jjj(dir, &["problem", "new", "Test problem"]);
run_jjj(
dir,
&[
"solution",
"new",
"Test solution",
"--problem",
"Test problem",
"--reviewer",
"bob",
],
);
let output = run_jjj(dir, &["status"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("REVIEW") || stdout.contains("review"),
"Expected REVIEW in status output: {}",
stdout
);
assert!(
stdout.contains("Awaiting review from @bob") || stdout.contains("Review requested"),
"Expected awaiting review info: {}",
stdout
);
}