use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn run_deciduous(args: &[&str], db_path: &PathBuf) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_deciduous"))
.args(args)
.env("DECIDUOUS_DB_PATH", db_path)
.output()
.expect("Failed to execute deciduous")
}
fn stdout(output: &std::process::Output) -> String {
String::from_utf8_lossy(&output.stdout).to_string()
}
fn stderr(output: &std::process::Output) -> String {
String::from_utf8_lossy(&output.stderr).to_string()
}
#[test]
fn test_help_command() {
let output = Command::new(env!("CARGO_BIN_EXE_deciduous"))
.arg("--help")
.output()
.expect("Failed to execute");
assert!(output.status.success());
let out = stdout(&output);
assert!(out.contains("deciduous"));
assert!(out.contains("Decision graph"));
}
#[test]
fn test_version_command() {
let output = Command::new(env!("CARGO_BIN_EXE_deciduous"))
.arg("--version")
.output()
.expect("Failed to execute");
assert!(output.status.success());
let out = stdout(&output);
assert!(out.contains("deciduous"));
}
#[test]
fn test_completion_zsh() {
let output = Command::new(env!("CARGO_BIN_EXE_deciduous"))
.args(["completion", "zsh"])
.output()
.expect("Failed to execute");
assert!(
output.status.success(),
"completion zsh failed: {}",
stderr(&output)
);
let out = stdout(&output);
assert!(
out.contains("#compdef deciduous"),
"zsh completion should contain #compdef"
);
}
#[test]
fn test_completion_bash() {
let output = Command::new(env!("CARGO_BIN_EXE_deciduous"))
.args(["completion", "bash"])
.output()
.expect("Failed to execute");
assert!(
output.status.success(),
"completion bash failed: {}",
stderr(&output)
);
let out = stdout(&output);
assert!(
out.contains("_deciduous"),
"bash completion should contain _deciduous function"
);
}
#[test]
fn test_completion_fish() {
let output = Command::new(env!("CARGO_BIN_EXE_deciduous"))
.args(["completion", "fish"])
.output()
.expect("Failed to execute");
assert!(
output.status.success(),
"completion fish failed: {}",
stderr(&output)
);
let out = stdout(&output);
assert!(
out.contains("complete -c deciduous"),
"fish completion should contain complete command"
);
}
#[test]
fn test_completion_help() {
let output = Command::new(env!("CARGO_BIN_EXE_deciduous"))
.args(["completion", "--help"])
.output()
.expect("Failed to execute");
assert!(output.status.success());
let out = stdout(&output);
assert!(out.contains("bash"));
assert!(out.contains("zsh"));
assert!(out.contains("fish"));
}
#[test]
fn test_add_and_list_nodes() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = run_deciduous(&["add", "goal", "Test Goal", "-c", "90"], &db_path);
assert!(
output.status.success(),
"add goal failed: {}",
stderr(&output)
);
assert!(stdout(&output).contains("Created node"));
let output = run_deciduous(&["add", "action", "Test Action", "-c", "85"], &db_path);
assert!(
output.status.success(),
"add action failed: {}",
stderr(&output)
);
let output = run_deciduous(&["nodes"], &db_path);
assert!(output.status.success(), "nodes failed: {}", stderr(&output));
let out = stdout(&output);
assert!(out.contains("Test Goal"));
assert!(out.contains("Test Action"));
assert!(out.contains("goal"));
assert!(out.contains("action"));
}
#[test]
fn test_add_node_with_all_metadata() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = run_deciduous(
&[
"add",
"goal",
"Full Metadata Goal",
"-c",
"95",
"-p",
"User asked: implement feature X",
"-f",
"src/main.rs,src/lib.rs",
"-b",
"feature-branch",
],
&db_path,
);
assert!(
output.status.success(),
"add with metadata failed: {}",
stderr(&output)
);
let output = run_deciduous(&["nodes"], &db_path);
assert!(stdout(&output).contains("Full Metadata Goal"));
}
#[test]
fn test_add_all_node_types() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let node_types = [
"goal",
"decision",
"option",
"action",
"outcome",
"observation",
];
for node_type in &node_types {
let title = format!("Test {}", node_type);
let output = run_deciduous(&["add", node_type, &title, "-c", "80"], &db_path);
assert!(
output.status.success(),
"add {} failed: {}",
node_type,
stderr(&output)
);
}
let output = run_deciduous(&["nodes"], &db_path);
let out = stdout(&output);
for node_type in &node_types {
assert!(out.contains(node_type), "Missing node type: {}", node_type);
}
}
#[test]
fn test_link_nodes() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Goal", "-c", "90"], &db_path);
run_deciduous(&["add", "action", "Action", "-c", "85"], &db_path);
let output = run_deciduous(&["link", "1", "2", "-r", "Goal leads to action"], &db_path);
assert!(output.status.success(), "link failed: {}", stderr(&output));
assert!(stdout(&output).contains("Created edge"));
let output = run_deciduous(&["edges"], &db_path);
assert!(output.status.success());
let out = stdout(&output);
assert!(out.contains("1"));
assert!(out.contains("2"));
assert!(out.contains("leads_to"));
}
#[test]
fn test_link_with_edge_types() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "decision", "Choose framework"], &db_path);
run_deciduous(&["add", "option", "React"], &db_path);
run_deciduous(&["add", "option", "Vue"], &db_path);
let output = run_deciduous(
&["link", "1", "2", "-t", "chosen", "-r", "Better ecosystem"],
&db_path,
);
assert!(output.status.success());
let output = run_deciduous(
&[
"link",
"1",
"3",
"-t",
"rejected",
"-r",
"Smaller community",
],
&db_path,
);
assert!(output.status.success());
let output = run_deciduous(&["edges"], &db_path);
let out = stdout(&output);
assert!(out.contains("chosen"));
assert!(out.contains("rejected"));
}
#[test]
fn test_update_node_status() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "action", "My Action"], &db_path);
let output = run_deciduous(&["status", "1", "completed"], &db_path);
assert!(
output.status.success(),
"status update failed: {}",
stderr(&output)
);
let output = run_deciduous(&["nodes"], &db_path);
assert!(stdout(&output).contains("completed"));
}
#[test]
fn test_graph_json_export() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Export Test Goal"], &db_path);
run_deciduous(&["add", "action", "Export Test Action"], &db_path);
run_deciduous(&["link", "1", "2", "-r", "test"], &db_path);
let output = run_deciduous(&["graph"], &db_path);
assert!(
output.status.success(),
"graph export failed: {}",
stderr(&output)
);
let out = stdout(&output);
let json: serde_json::Value = serde_json::from_str(&out).expect("Output should be valid JSON");
assert!(json.get("nodes").is_some(), "JSON should have nodes");
assert!(json.get("edges").is_some(), "JSON should have edges");
let nodes = json["nodes"].as_array().unwrap();
assert_eq!(nodes.len(), 2);
}
#[test]
fn test_dot_export() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "DOT Test"], &db_path);
run_deciduous(&["add", "action", "DOT Action"], &db_path);
run_deciduous(&["link", "1", "2"], &db_path);
let output = run_deciduous(&["dot"], &db_path);
assert!(
output.status.success(),
"dot export failed: {}",
stderr(&output)
);
let out = stdout(&output);
assert!(out.contains("digraph"));
assert!(out.contains("DOT Test"));
assert!(out.contains("->"));
}
#[test]
fn test_filter_nodes_by_type() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Goal 1"], &db_path);
run_deciduous(&["add", "goal", "Goal 2"], &db_path);
run_deciduous(&["add", "action", "Action 1"], &db_path);
let output = run_deciduous(&["nodes", "-t", "goal"], &db_path);
assert!(output.status.success());
let out = stdout(&output);
assert!(out.contains("Goal 1"));
assert!(out.contains("Goal 2"));
assert!(!out.contains("Action 1"));
}
#[test]
fn test_command_log() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Logged Goal"], &db_path);
run_deciduous(&["add", "action", "Logged Action"], &db_path);
let output = run_deciduous(&["commands"], &db_path);
assert!(
output.status.success(),
"commands failed: {}",
stderr(&output)
);
let out = stdout(&output);
assert!(!out.is_empty());
}
#[test]
fn test_link_nonexistent_nodes() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = run_deciduous(&["link", "999", "998"], &db_path);
assert!(
!output.status.success()
|| stderr(&output).contains("Error")
|| stderr(&output).contains("not found")
);
}
#[test]
fn test_invalid_node_type() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = run_deciduous(&["add", "invalid_type", "Test"], &db_path);
let _out = stdout(&output);
let _err = stderr(&output);
}
#[test]
fn test_delete_node() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Delete Me", "-c", "90"], &db_path);
let output = run_deciduous(&["nodes"], &db_path);
assert!(stdout(&output).contains("Delete Me"));
let output = run_deciduous(&["delete", "1"], &db_path);
assert!(
output.status.success(),
"delete failed: {}",
stderr(&output)
);
let output = run_deciduous(&["nodes"], &db_path);
assert!(!stdout(&output).contains("Delete Me"));
}
#[test]
fn test_delete_node_cascades_edges() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Parent"], &db_path);
run_deciduous(&["add", "action", "Child"], &db_path);
run_deciduous(&["link", "1", "2", "-r", "parent-child"], &db_path);
let output = run_deciduous(&["edges"], &db_path);
assert!(stdout(&output).contains("leads_to"));
let output = run_deciduous(&["delete", "1"], &db_path);
assert!(
output.status.success(),
"delete failed: {}",
stderr(&output)
);
let output = run_deciduous(&["edges"], &db_path);
let out = stdout(&output);
assert!(
!out.contains("parent-child"),
"Edge should be removed after node delete"
);
}
#[test]
fn test_unlink_nodes() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Goal"], &db_path);
run_deciduous(&["add", "action", "Action"], &db_path);
run_deciduous(&["link", "1", "2", "-r", "test link"], &db_path);
let output = run_deciduous(&["edges"], &db_path);
assert!(stdout(&output).contains("1"));
let output = run_deciduous(&["unlink", "1", "2"], &db_path);
assert!(
output.status.success(),
"unlink failed: {}",
stderr(&output)
);
}
#[test]
fn test_show_node_detail() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(
&[
"add",
"goal",
"Detailed Goal",
"-c",
"95",
"-p",
"User prompt here",
],
&db_path,
);
let output = run_deciduous(&["show", "1"], &db_path);
assert!(output.status.success(), "show failed: {}", stderr(&output));
let out = stdout(&output);
assert!(out.contains("Detailed Goal"));
}
#[test]
fn test_show_node_json() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "JSON Goal", "-c", "90"], &db_path);
let output = run_deciduous(&["show", "1", "--json"], &db_path);
assert!(
output.status.success(),
"show --json failed: {}",
stderr(&output)
);
let out = stdout(&output);
let json: serde_json::Value =
serde_json::from_str(&out).expect("show --json should output valid JSON");
assert_eq!(json["title"], "JSON Goal");
}
#[test]
fn test_update_prompt() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Prompt Goal"], &db_path);
let output = run_deciduous(&["prompt", "1", "Updated prompt text"], &db_path);
assert!(
output.status.success(),
"prompt update failed: {}",
stderr(&output)
);
let output = run_deciduous(&["show", "1", "--json"], &db_path);
let out = stdout(&output);
assert!(out.contains("Updated prompt text"));
}
#[test]
fn test_all_status_values() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Status Test"], &db_path);
let statuses = ["active", "completed", "superseded", "abandoned", "rejected"];
for status in &statuses {
let output = run_deciduous(&["status", "1", status], &db_path);
assert!(
output.status.success(),
"status {} failed: {}",
status,
stderr(&output)
);
}
}
#[test]
fn test_complex_graph_structure() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Root Goal", "-c", "90"], &db_path);
run_deciduous(&["add", "option", "Option A", "-c", "80"], &db_path);
run_deciduous(&["add", "option", "Option B", "-c", "75"], &db_path);
run_deciduous(&["add", "decision", "Choose A", "-c", "95"], &db_path);
run_deciduous(&["add", "action", "Implement A", "-c", "85"], &db_path);
run_deciduous(&["add", "outcome", "A works", "-c", "90"], &db_path);
run_deciduous(&["add", "observation", "Noticed X", "-c", "70"], &db_path);
run_deciduous(&["link", "1", "2", "-r", "possible approach"], &db_path);
run_deciduous(&["link", "1", "3", "-r", "possible approach"], &db_path);
run_deciduous(
&["link", "2", "4", "-t", "chosen", "-r", "better fit"],
&db_path,
);
run_deciduous(
&["link", "3", "4", "-t", "rejected", "-r", "too complex"],
&db_path,
);
run_deciduous(&["link", "4", "5", "-r", "implementation"], &db_path);
run_deciduous(&["link", "5", "6", "-r", "result"], &db_path);
run_deciduous(&["link", "7", "1", "-r", "context"], &db_path);
let output = run_deciduous(&["graph"], &db_path);
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_str(&stdout(&output)).expect("Graph should be valid JSON");
assert_eq!(json["nodes"].as_array().unwrap().len(), 7);
assert_eq!(json["edges"].as_array().unwrap().len(), 7);
let output = run_deciduous(&["dot"], &db_path);
assert!(output.status.success());
let dot = stdout(&output);
assert!(dot.contains("Root Goal"));
assert!(dot.contains("Option A"));
assert!(dot.contains("Choose A"));
assert!(dot.contains("Implement A"));
assert!(dot.contains("A works"));
assert!(dot.contains("Noticed X"));
}
#[test]
fn test_dot_with_root_filter() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Chain 1 Root"], &db_path);
run_deciduous(&["add", "action", "Chain 1 Action"], &db_path);
run_deciduous(&["link", "1", "2"], &db_path);
run_deciduous(&["add", "goal", "Chain 2 Root"], &db_path);
run_deciduous(&["add", "action", "Chain 2 Action"], &db_path);
run_deciduous(&["link", "3", "4"], &db_path);
let output = run_deciduous(&["dot", "-r", "1"], &db_path);
assert!(output.status.success());
let dot = stdout(&output);
assert!(dot.contains("Chain 1 Root"));
assert!(dot.contains("Chain 1 Action"));
assert!(!dot.contains("Chain 2 Root"));
}
#[test]
fn test_dot_with_node_range() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Node 1"], &db_path);
run_deciduous(&["add", "action", "Node 2"], &db_path);
run_deciduous(&["add", "outcome", "Node 3"], &db_path);
let output = run_deciduous(&["dot", "-n", "1-2"], &db_path);
assert!(output.status.success());
let dot = stdout(&output);
assert!(dot.contains("Node 1"));
assert!(dot.contains("Node 2"));
assert!(!dot.contains("Node 3"));
}
#[test]
fn test_backup() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Backup Test"], &db_path);
let output = run_deciduous(&["backup"], &db_path);
assert!(
output.status.success(),
"backup failed: {}",
stderr(&output)
);
}
#[test]
fn test_empty_db_nodes() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = run_deciduous(&["nodes"], &db_path);
assert!(
output.status.success(),
"nodes on empty db failed: {}",
stderr(&output)
);
}
#[test]
fn test_empty_db_edges() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = run_deciduous(&["edges"], &db_path);
assert!(
output.status.success(),
"edges on empty db failed: {}",
stderr(&output)
);
}
#[test]
fn test_empty_db_graph() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = run_deciduous(&["graph"], &db_path);
assert!(
output.status.success(),
"graph on empty db failed: {}",
stderr(&output)
);
let json: serde_json::Value =
serde_json::from_str(&stdout(&output)).expect("Empty graph should be valid JSON");
assert_eq!(json["nodes"].as_array().unwrap().len(), 0);
assert_eq!(json["edges"].as_array().unwrap().len(), 0);
}
#[test]
fn test_filter_nodes_by_branch() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "Main Goal", "-b", "main"], &db_path);
run_deciduous(
&["add", "goal", "Feature Goal", "-b", "feature-x"],
&db_path,
);
let output = run_deciduous(&["nodes", "-b", "main"], &db_path);
assert!(output.status.success());
let out = stdout(&output);
assert!(out.contains("Main Goal"));
assert!(!out.contains("Feature Goal"));
}
#[test]
fn test_writeup_basic() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "goal", "PR Goal", "-c", "90"], &db_path);
run_deciduous(&["add", "action", "PR Action", "-c", "85"], &db_path);
run_deciduous(&["link", "1", "2", "-r", "implementation"], &db_path);
let output = run_deciduous(&["writeup", "-t", "Test PR", "-n", "1-2"], &db_path);
assert!(
output.status.success(),
"writeup failed: {}",
stderr(&output)
);
let out = stdout(&output);
assert!(out.contains("Test PR"));
}
#[test]
fn test_all_edge_types() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
for i in 0..6 {
run_deciduous(&["add", "goal", &format!("Node {}", i)], &db_path);
}
let edge_types = [
"leads_to", "chosen", "rejected", "blocks", "enables", "requires",
];
for (i, edge_type) in edge_types.iter().enumerate() {
let from = format!("{}", i + 1);
let to = format!("{}", ((i + 1) % 6) + 1);
let output = run_deciduous(
&[
"link",
&from,
&to,
"-t",
edge_type,
"-r",
&format!("test {}", edge_type),
],
&db_path,
);
assert!(
output.status.success(),
"link with type {} failed: {}",
edge_type,
stderr(&output)
);
}
let output = run_deciduous(&["edges"], &db_path);
let out = stdout(&output);
for edge_type in &edge_types {
assert!(out.contains(edge_type), "Missing edge type: {}", edge_type);
}
}
#[test]
fn test_revisit_node_type() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = run_deciduous(
&["add", "revisit", "Reconsider approach", "-c", "80"],
&db_path,
);
assert!(
output.status.success(),
"add revisit failed: {}",
stderr(&output)
);
assert!(stdout(&output).contains("Created node"));
let output = run_deciduous(&["nodes"], &db_path);
assert!(stdout(&output).contains("revisit"));
}
#[test]
fn test_supersede_node() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
run_deciduous(&["add", "decision", "Old Approach"], &db_path);
run_deciduous(&["add", "decision", "New Approach"], &db_path);
let output = run_deciduous(&["status", "1", "superseded"], &db_path);
assert!(
output.status.success(),
"supersede failed: {}",
stderr(&output)
);
let output = run_deciduous(&["show", "1", "--json"], &db_path);
let out = stdout(&output);
assert!(out.contains("superseded"));
}